terraform/internal/stacks/stackruntime/apply_test.go

4825 lines
174 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackruntime
import (
"context"
2024-06-21 08:05:26 -04:00
"fmt"
"path"
"path/filepath"
"sort"
"strconv"
2024-06-21 08:05:26 -04:00
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform"
"github.com/hashicorp/terraform/internal/collections"
2024-06-27 10:08:08 -04:00
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
"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"
)
var changesCmpOpts = cmp.Options{
ctydebug.CmpOptions,
collections.CmpOptions,
cmpopts.IgnoreUnexported(addrs.InputVariable{}),
cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{}),
}
// TestApply uses a generic framework for running apply 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 TestApply(t *testing.T) {
fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z")
if err != nil {
t.Fatal(err)
}
tcs := map[string]struct {
path string
skip bool
state *stackstate.State
store *stacks_testing_provider.ResourceStore
cycles []TestCycle
}{
"built-in provider used not present in required": {
path: "with-built-in-provider",
cycles: []TestCycle{
{}, // plan, apply -> no diags
},
},
"built-in provider used and explicitly defined in required providers": {
path: "with-built-in-provider-explicitly-defined",
cycles: []TestCycle{
{}, // plan, apply -> no diags
},
},
"creating inputs and outputs": {
path: "component-input-output",
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"value": cty.StringVal("foo"),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: mustStackOutputValue("value"),
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("foo"),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: mustStackInputVariable("value"),
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("foo"),
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeOutputValue{
Addr: mustStackOutputValue("value"),
Value: cty.StringVal("foo"),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("value"),
Value: cty.StringVal("foo"),
},
},
},
},
},
"updating inputs and outputs": {
path: "component-input-output",
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"value": cty.StringVal("foo"),
},
},
{
planInputs: map[string]cty.Value{
"value": cty.StringVal("bar"),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: mustStackOutputValue("value"),
Action: plans.Update,
Before: cty.StringVal("foo"),
After: cty.StringVal("bar"),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: mustStackInputVariable("value"),
Action: plans.Update,
Before: cty.StringVal("foo"),
After: cty.StringVal("bar"),
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeOutputValue{
Addr: mustStackOutputValue("value"),
Value: cty.StringVal("bar"),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("value"),
Value: cty.StringVal("bar"),
},
},
},
},
},
"updating inputs and outputs (noop)": {
path: "component-input-output",
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"value": cty.StringVal("foo"),
},
},
{
planInputs: map[string]cty.Value{
"value": cty.StringVal("foo"),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: mustStackOutputValue("value"),
Action: plans.NoOp,
Before: cty.StringVal("foo"),
After: cty.StringVal("foo"),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: mustStackInputVariable("value"),
Action: plans.NoOp,
Before: cty.StringVal("foo"),
After: cty.StringVal("foo"),
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeOutputValue{
Addr: mustStackOutputValue("value"),
Value: cty.StringVal("foo"),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("value"),
Value: cty.StringVal("foo"),
},
},
},
},
},
"deleting inputs and outputs": {
path: "component-input-output",
state: stackstate.NewStateBuilder().
AddInput("removed", cty.StringVal("bar")).
AddOutput("removed", cty.StringVal("bar")).
Build(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"value": cty.StringVal("foo"),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: mustStackOutputValue("removed"),
Action: plans.Delete,
Before: cty.StringVal("bar"),
After: cty.NullVal(cty.DynamicPseudoType),
},
&stackplan.PlannedChangeOutputValue{
Addr: mustStackOutputValue("value"),
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("foo"),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: mustStackInputVariable("removed"),
Action: plans.Delete,
Before: cty.StringVal("bar"),
After: cty.NullVal(cty.DynamicPseudoType),
},
&stackplan.PlannedChangeRootInputValue{
Addr: mustStackInputVariable("value"),
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("foo"),
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeOutputValue{
Addr: mustStackOutputValue("removed"),
},
&stackstate.AppliedChangeOutputValue{
Addr: mustStackOutputValue("value"),
Value: cty.StringVal("foo"),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed"),
Value: cty.NilVal, // destroyed
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("value"),
Value: cty.StringVal("foo"),
},
},
},
},
},
"checkable objects": {
path: "checkable-objects",
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.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},
},
})
}),
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.single"),
ComponentInstanceAddr: mustAbsComponentInstance("component.single"),
OutputValues: map[addrs.OutputValue]cty.Value{
addrs.OutputValue{Name: "foo"}: cty.StringVal("bar"),
},
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("foo"): cty.StringVal("bar"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.main"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "test",
"value": "bar",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.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},
},
})
}),
},
{
planMode: plans.DestroyMode,
planInputs: map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.single"),
ComponentInstanceAddr: mustAbsComponentInstance("component.single"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.main"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
},
},
},
},
"removed component": {
path: filepath.Join("with-single-input", "removed-component"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")).
AddInputVariable("id", cty.StringVal("removed")).
AddInputVariable("input", cty.StringVal("removed"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")).
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(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
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.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,
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
},
},
},
},
"removed component instance": {
path: filepath.Join("with-single-input", "removed-component-instance"),
state: 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",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("removed", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("removed"),
"value": cty.StringVal("removed"),
})).
Build(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"input": cty.SetVal([]cty.Value{
cty.StringVal("added"),
}),
"removed": cty.SetVal([]cty.Value{
cty.StringVal("removed"),
}),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
// 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"),
}),
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"added\"]"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("added"),
mustInputVariable("input"): cty.StringVal("added"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "added",
"value": "added",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"removed\"]"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.SetVal([]cty.Value{
cty.StringVal("added"),
}),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed"),
Value: cty.SetVal([]cty.Value{
cty.StringVal("removed"),
}),
},
},
},
},
},
"duplicate removed blocks": {
path: path.Join("with-single-input", "removed-component-duplicate"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"one\"]")).
AddInputVariable("id", cty.StringVal("one")).
AddInputVariable("input", cty.StringVal("one"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "one",
"value": "one",
}),
})).
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"two\"]")).
AddInputVariable("id", cty.StringVal("two")).
AddInputVariable("input", cty.StringVal("two"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "two",
"value": "two",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("one", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("one"),
"value": cty.StringVal("one"),
})).
AddResource("two", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("two"),
"value": cty.StringVal("two"),
})).
Build(),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
planInputs: map[string]cty.Value{
"input": cty.SetValEmpty(cty.String),
"removed_one": cty.SetVal([]cty.Value{
cty.StringVal("one"),
}),
"removed_two": cty.SetVal([]cty.Value{
cty.StringVal("two"),
}),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"one\"]"),
PlanComplete: true,
PlanApplyable: true,
Mode: plans.DestroyMode,
Action: plans.Delete,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("one")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("one")),
},
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[\"one\"].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("one"),
"value": cty.StringVal("one"),
})),
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": "one",
"value": "one",
}),
Dependencies: make([]addrs.ConfigResource, 0),
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self[\"two\"]"),
PlanComplete: true,
PlanApplyable: true,
Mode: plans.DestroyMode,
Action: plans.Delete,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("two")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("two")),
},
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[\"two\"].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("two"),
"value": cty.StringVal("two"),
})),
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": "two",
"value": "two",
}),
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.SetValEmpty(cty.String),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed_one"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("one"),
}),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "removed_two"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.SetVal([]cty.Value{
cty.StringVal("two"),
}),
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"one\"]"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"two\"]"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.SetValEmpty(cty.String),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed_one"),
Value: cty.SetVal([]cty.Value{
cty.StringVal("one"),
}),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed_two"),
Value: cty.SetVal([]cty.Value{
cty.StringVal("two"),
}),
},
},
},
},
},
"removed component instance direct": {
path: filepath.Join("with-single-input", "removed-component-instance-direct"),
state: 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",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("removed", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("removed"),
"value": cty.StringVal("removed"),
})).
Build(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"input": cty.SetVal([]cty.Value{
cty.StringVal("added"),
}),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
// 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"),
}),
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"added\"]"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("added"),
mustInputVariable("input"): cty.StringVal("added"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "added",
"value": "added",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"removed\"]"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.SetVal([]cty.Value{
cty.StringVal("added"),
}),
},
},
},
},
},
"removed stack instance": {
path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"removed\"].component.self")).
AddInputVariable("id", cty.StringVal("removed")).
AddInputVariable("input", cty.StringVal("removed"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("stack.simple[\"removed\"].component.self.testing_resource.data")).
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(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"input": cty.MapVal(map[string]cty.Value{
"added": cty.StringVal("added"),
}),
"removed": cty.MapVal(map[string]cty.Value{
"removed": cty.StringVal("removed"),
}),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("stack.simple[\"added\"].component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("stack.simple[\"added\"].component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("added"),
mustInputVariable("input"): cty.StringVal("added"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"added\"].component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "added",
"value": "added",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("stack.simple[\"removed\"].component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("stack.simple[\"removed\"].component.self"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"removed\"].component.self.testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.MapVal(map[string]cty.Value{
"added": cty.StringVal("added"),
}),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed"),
Value: cty.MapVal(map[string]cty.Value{
"removed": cty.StringVal("removed"),
}),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed-direct"),
Value: cty.SetValEmpty(cty.String),
},
},
},
},
},
"removed embedded dynamic component from stack": {
path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.for_each.component.self[\"removed\"]")).
AddInputVariable("id", cty.StringVal("removed")).
AddInputVariable("input", cty.StringVal("removed"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("stack.for_each.component.self[\"removed\"].testing_resource.data")).
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(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"for_each_input": cty.MapVal(map[string]cty.Value{
"added": cty.StringVal("added"),
}),
"for_each_removed": cty.SetVal([]cty.Value{
cty.StringVal("removed"),
}),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("stack.for_each.component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("stack.for_each.component.self[\"added\"]"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("added"),
mustInputVariable("input"): cty.StringVal("added"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.for_each.component.self[\"added\"].testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "added",
"value": "added",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("stack.for_each.component.self[\"removed\"]"),
ComponentInstanceAddr: mustAbsComponentInstance("stack.for_each.component.self[\"removed\"]"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.for_each.component.self[\"removed\"].testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("for_each_input"),
Value: cty.MapVal(map[string]cty.Value{
"added": cty.StringVal("added"),
}),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("for_each_removed"),
Value: cty.SetVal([]cty.Value{
cty.StringVal("removed"),
}),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("simple_input"),
Value: cty.MapValEmpty(cty.String),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("simple_removed"),
Value: cty.SetValEmpty(cty.String),
},
},
},
},
},
"removed embedded component relative": {
path: filepath.Join("with-single-input", "removed-component-from-stack"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.nested.component.self[\"foo\"]")).
AddInputVariable("id", cty.StringVal("removed")).
AddInputVariable("input", cty.StringVal("removed"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("stack.nested.component.self[\"foo\"].testing_resource.data")).
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(),
cycles: []TestCycle{
{
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("stack.nested.component.self[\"foo\"]"),
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("stack.nested.component.self[\"foo\"].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,
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("stack.nested.component.self[\"foo\"]"),
ComponentInstanceAddr: mustAbsComponentInstance("stack.nested.component.self[\"foo\"]"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.nested.component.self[\"foo\"].testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
},
},
},
},
"removed embedded component local": {
path: filepath.Join("with-single-input", "removed-embedded-component"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.a.component.self")).
AddInputVariable("id", cty.StringVal("removed")).
AddInputVariable("input", cty.StringVal("removed"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("stack.a.component.self.testing_resource.data")).
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(),
cycles: []TestCycle{
{
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("stack.a.component.self"),
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("stack.a.component.self.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,
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("stack.a.component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("stack.a.component.self"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.a.component.self.testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
},
},
},
},
"forgotten component": {
path: filepath.Join("with-single-input", "forgotten-component"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")).
AddInputVariable("id", cty.StringVal("removed")).
AddInputVariable("input", cty.StringVal("removed"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")).
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(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"destroy": cty.BoolVal(false),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanComplete: true,
PlanApplyable: true,
Mode: plans.DestroyMode,
Action: plans.Forget,
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.testing_resource.data"),
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: mustAbsResourceInstance("testing_resource.data"),
PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
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,
}))),
},
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,
},
},
wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Some objects will no longer be managed by Terraform",
`If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:
- testing_resource.data
After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`,
))
}),
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
Schema: providers.Schema{},
},
},
},
},
},
"orphaned component": {
path: filepath.Join("with-single-input", "valid"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.orphan"))).
Build(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"id": cty.StringVal("foo"),
"input": cty.StringVal("bar"),
},
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstanceRemoved{
// The orphaned component is just silently being removed.
Addr: mustAbsComponentInstance("component.orphan"),
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("foo")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("bar")),
2024-06-27 12:01:20 -04:00
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: &states.CheckResults{},
PlanTimestamp: fakePlanTimestamp,
2024-06-27 12:01:20 -04:00
},
&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.Create,
Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"value": cty.StringVal("bar"),
})),
2024-06-27 12:01:20 -04:00
},
},
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("foo"),
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{
Name: "input",
},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("bar"),
2024-06-27 12:01:20 -04:00
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
// The orphaned component is just silently being removed.
ComponentAddr: mustAbsComponent("component.orphan"),
ComponentInstanceAddr: mustAbsComponentInstance("component.orphan"),
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("foo"),
mustInputVariable("input"): cty.StringVal("bar"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "foo",
"value": "bar",
}),
Dependencies: make([]addrs.ConfigResource, 0),
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("id"),
Value: cty.StringVal("foo"),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.StringVal("bar"),
},
},
},
2024-06-27 12:01:20 -04:00
},
},
"forget with dependency": {
path: "forget_with_dependency",
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.one")).
AddDependent(mustAbsComponent("component.two")).
AddInputVariable("value", cty.StringVal("bar")).
AddOutputValue("id", cty.StringVal("foo"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.one.testing_resource.resource")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "foo",
"value": "bar",
}),
Status: states.ObjectReady,
})).
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.two")).
AddDependency(mustAbsComponent("component.one")).
AddInputVariable("value", cty.StringVal("foo")).
AddOutputValue("id", cty.StringVal("baz"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.two.testing_resource.resource")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "baz",
"value": "foo",
}),
Status: states.ObjectReady,
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("foo", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"value": cty.StringVal("bar"),
})).
AddResource("baz", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("baz"),
"value": cty.StringVal("foo"),
})).
Build(),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
wantPlannedDiags: tfdiags.Diagnostics{
tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:
- testing_resource.resource
2024-06-27 12:01:20 -04:00
After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.one"),
ComponentInstanceAddr: mustAbsComponentInstance("component.one"),
OutputValues: map[addrs.OutputValue]cty.Value{
addrs.OutputValue{Name: "id"}: cty.StringVal("foo"),
},
InputVariables: map[addrs.InputVariable]cty.Value{
addrs.InputVariable{Name: "value"}: cty.StringVal("bar"),
},
Dependents: collections.NewSet(mustAbsComponent("component.two")),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.resource"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "foo",
"value": "bar",
}),
Status: states.ObjectReady,
AttrSensitivePaths: make([]cty.Path, 0),
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.two"),
ComponentInstanceAddr: mustAbsComponentInstance("component.two"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.two.testing_resource.resource"),
NewStateSrc: nil, // Resource is forgotten
ProviderConfigAddr: addrs.AbsProviderConfig{
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
},
},
},
},
"forget with dependency on component to forget": {
path: "forget_with_dependency_to_forget",
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.one")).
AddDependent(mustAbsComponent("component.two")).
AddInputVariable("value", cty.StringVal("bar")).
AddOutputValue("id", cty.StringVal("foo"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.one.testing_resource.resource")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "foo",
"value": "bar",
}),
Status: states.ObjectReady,
})).
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.two")).
AddDependency(mustAbsComponent("component.one")).
AddInputVariable("value", cty.StringVal("foo")).
AddOutputValue("id", cty.StringVal("baz"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.two.testing_resource.resource")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "baz",
"value": "foo",
}),
Status: states.ObjectReady,
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("foo", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"value": cty.StringVal("bar"),
})).
AddResource("baz", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("baz"),
"value": cty.StringVal("foo"),
})).
Build(),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
wantPlannedDiags: tfdiags.Diagnostics{
tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:
- testing_resource.resource
After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`),
tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:
- testing_resource.resource
After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.one"),
ComponentInstanceAddr: mustAbsComponentInstance("component.one"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.resource"),
NewStateSrc: nil, // Resource is forgotten
ProviderConfigAddr: addrs.AbsProviderConfig{
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.two"),
ComponentInstanceAddr: mustAbsComponentInstance("component.two"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.two.testing_resource.resource"),
NewStateSrc: nil, // Resource is forgotten
ProviderConfigAddr: addrs.AbsProviderConfig{
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
},
},
},
},
2024-06-27 12:01:20 -04:00
},
"removed block with provider-to-component dep": {
path: path.Join("auth-provider-w-data", "removed"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.load")).
AddDependent(mustAbsComponent("component.create")).
AddOutputValue("credentials", cty.StringVal("wrong"))). // must reload the credentials
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.create")).
AddDependency(mustAbsComponent("component.load"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.create.testing_resource.resource")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "resource",
"value": nil,
}),
Status: states.ObjectReady,
}).
SetProviderAddr(mustDefaultRootProvider("testing"))).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().AddResource("credentials", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("credentials"),
// we have the wrong value in state, so this correct value must
// be loaded for this test to work.
"value": cty.StringVal("authn"),
})).Build(),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.create"),
ComponentInstanceAddr: mustAbsComponentInstance("component.create"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.create.testing_resource.resource"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil, // deleted
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.load"),
ComponentInstanceAddr: mustAbsComponentInstance("component.load"),
OutputValues: map[addrs.OutputValue]cty.Value{
addrs.OutputValue{Name: "credentials"}: cty.StringVal("authn").Mark(marks.Sensitive),
},
InputVariables: make(map[addrs.InputVariable]cty.Value),
Dependents: collections.NewSet(mustAbsComponent("component.create")),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.load.data.testing_data_source.credentials"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "credentials",
"value": "authn",
}),
AttrSensitivePaths: make([]cty.Path, 0),
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingDataSourceSchema,
},
},
},
},
},
"ephemeral": {
path: path.Join("with-single-input", "ephemeral"),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
planInputs: map[string]cty.Value{
"input": cty.StringVal("hello"),
"ephemeral": cty.StringVal("planning"),
},
applyInputs: map[string]cty.Value{
"ephemeral": cty.StringVal("applying"),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("2f9f3b84"),
mustInputVariable("input"): cty.StringVal("hello"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "2f9f3b84",
"value": "hello",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("ephemeral"),
Value: cty.NullVal(cty.String), // ephemeral
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.StringVal("hello"),
},
},
},
},
},
"missing-ephemeral": {
path: path.Join("with-single-input", "ephemeral"),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
planInputs: map[string]cty.Value{
"input": cty.StringVal("hello"),
"ephemeral": cty.StringVal("planning"),
},
applyInputs: make(map[string]cty.Value), // deliberately omitting ephemeral
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("2f9f3b84"),
mustInputVariable("input"): cty.StringVal("hello"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "2f9f3b84",
"value": "hello",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.StringVal("hello"),
},
},
wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No value for required variable",
Detail: "The root input variable \"var.ephemeral\" is not set, and has no default value.",
Subject: &hcl.Range{
Filename: "git::https://example.com/test.git//with-single-input/ephemeral/ephemeral.tfcomponent.hcl",
Start: hcl.Pos{
Line: 14,
Column: 1,
Byte: 175,
},
End: hcl.Pos{
Line: 14,
Column: 21,
Byte: 195,
},
},
})
}),
},
},
},
"ephemeral-default": {
path: path.Join("with-single-input", "ephemeral-default"),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
planInputs: map[string]cty.Value{
"input": cty.StringVal("hello"),
// deliberately omitting ephemeral
},
applyInputs: make(map[string]cty.Value), // deliberately omitting ephemeral
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("2f9f3b84"),
mustInputVariable("input"): cty.StringVal("hello"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "2f9f3b84",
"value": "hello",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("ephemeral"),
Value: cty.NullVal(cty.String), // ephemeral
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.StringVal("hello"),
},
},
},
},
},
"deferred-components": {
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(),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
planInputs: map[string]cty.Value{
"providers": cty.UnknownVal(cty.Set(cty.String)),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.const"),
ComponentInstanceAddr: mustAbsComponentInstance("component.const"),
Dependencies: collections.NewSet[stackaddrs.AbsComponent](),
Dependents: collections.NewSet[stackaddrs.AbsComponent](),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("data_known"),
mustInputVariable("resource"): cty.StringVal("resource_known"),
},
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "data_known",
"value": "known",
}),
AttrSensitivePaths: make([]cty.Path, 0),
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingDataSourceSchema,
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "resource_known",
"value": "known",
}),
Dependencies: []addrs.ConfigResource{
mustAbsResourceInstance("data.testing_data_source.data").ConfigResource(),
},
Status: states.ObjectReady,
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("providers"),
Value: cty.UnknownVal(cty.Set(cty.String)),
},
},
},
{
planMode: plans.DestroyMode,
planInputs: map[string]cty.Value{
"providers": cty.UnknownVal(cty.Set(cty.String)),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.const"),
ComponentInstanceAddr: mustAbsComponentInstance("component.const"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("providers"),
Value: cty.NilVal, // destroyed
},
},
},
},
},
"unknown-component-input": {
path: path.Join("map-object-input", "for-each-input"),
cycles: []TestCycle{
{
planMode: plans.NormalMode,
planInputs: map[string]cty.Value{
"inputs": cty.UnknownVal(cty.Map(cty.String)),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.main"),
ComponentInstanceAddr: mustAbsComponentInstance("component.main"),
Dependencies: collections.NewSet(mustAbsComponent("component.self")),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("input"): cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
"output": cty.String,
}))),
},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("inputs"),
Value: cty.UnknownVal(cty.Map(cty.String)),
},
},
},
{
planMode: plans.DestroyMode,
planInputs: map[string]cty.Value{
"inputs": cty.MapValEmpty(cty.String),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstanceRemoved{
ComponentAddr: mustAbsComponent("component.main"),
ComponentInstanceAddr: mustAbsComponentInstance("component.main"),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("inputs"),
Value: cty.NilVal, // destroyed
},
},
},
},
},
"unknown-component": {
path: path.Join("with-single-input", "removed-component-instance"),
state: stackstate.NewStateBuilder().
AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"main\"]")).
AddInputVariable("id", cty.StringVal("main")).
AddInputVariable("input", cty.StringVal("main"))).
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self[\"main\"].testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "main",
"value": "main",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("main", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("main"),
"value": cty.StringVal("main"),
})).
Build(),
cycles: []TestCycle{
{
planInputs: map[string]cty.Value{
"input": cty.UnknownVal(cty.Set(cty.String)),
"removed": cty.SetValEmpty(cty.String),
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"main\"]"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("main"),
mustInputVariable("input"): cty.StringVal("main"),
},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.UnknownVal(cty.Set(cty.String)),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("removed"),
Value: cty.SetValEmpty(cty.String),
},
},
},
},
},
"ephemeral-module-outputs": {
path: "ephemeral-module-output",
skip: true, // TODO(issues/37822): Enable this.
cycles: []TestCycle{
{
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.ephemeral_in"),
PlanApplyable: false,
PlanComplete: true,
Action: plans.Create,
RequiredComponents: collections.NewSet(mustAbsComponent("component.ephemeral_out")),
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: make(map[string]cty.Value),
PlannedCheckResults: new(states.CheckResults),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.ephemeral_out"),
PlanApplyable: false,
PlanComplete: true,
Action: plans.Create,
PlannedInputValues: make(map[string]plans.DynamicValue),
PlannedOutputValues: map[string]cty.Value{
"value": cty.DynamicVal, // ephemeral
},
PlannedCheckResults: new(states.CheckResults),
PlanTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
},
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.ephemeral_in"),
ComponentInstanceAddr: mustAbsComponentInstance("component.ephemeral_in"),
Dependencies: collections.NewSet[stackaddrs.AbsComponent](
mustAbsComponent("component.ephemeral_out"),
),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("input"): cty.UnknownVal(cty.String), // ephemeral
},
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.ephemeral_out"),
ComponentInstanceAddr: mustAbsComponentInstance("component.ephemeral_out"),
Dependents: collections.NewSet[stackaddrs.AbsComponent](
mustAbsComponent("component.ephemeral_in"),
),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
},
},
},
},
2024-06-27 12:01:20 -04:00
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
ctx := context.Background()
2024-06-27 12:01:20 -04:00
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) {
provider := stacks_testing_provider.NewProviderWithData(t, store)
provider.Authentication = "authn"
return provider, nil
},
},
dependencyLocks: *lock,
}
state := tc.state
for ix, cycle := range tc.cycles {
t.Run(strconv.FormatInt(int64(ix), 10), func(t *testing.T) {
var plan *stackplan.Plan
t.Run("plan", func(t *testing.T) {
plan = testContext.Plan(t, ctx, state, cycle)
})
t.Run("apply", func(t *testing.T) {
state = testContext.Apply(t, ctx, plan, cycle)
})
})
}
})
2024-06-27 12:01:20 -04:00
}
}
func TestApplyWithRemovedResource(t *testing.T) {
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", "valid-providers"))
2024-06-27 10:08:08 -04:00
lock := depsfile.NewLocks()
planReq := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
2024-06-27 10:08:08 -04:00
DependencyLocks: *lock,
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: mustMarshalJSONAttrs(map[string]interface{}{
"id": "FE1D5830765C",
"input": map[string]interface{}{
"value": "hello",
"type": "string",
},
"output": map[string]interface{}{
"value": nil,
"type": "string",
},
"triggers_replace": nil,
}),
Status: states.ObjectReady,
}).
SetProviderAddr(addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"),
})).
Build(),
}
planChangesCh := make(chan stackplan.PlannedChange)
diagsCh := make(chan tfdiags.Diagnostic)
planResp := PlanResponse{
PlannedChanges: planChangesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &planReq, &planResp)
planChanges, diags := collectPlanOutput(planChangesCh, diagsCh)
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, go %s", diags.ErrWithWarnings())
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
if len(applyDiags) > 0 {
t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings())
}
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.terraform_data.main"),
NewStateSrc: nil, // Deleted, so is nil.
ProviderConfigAddr: addrs.AbsProviderConfig{
Provider: addrs.Provider{
Type: "terraform",
Namespace: "builtin",
Hostname: "terraform.io",
},
},
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyWithMovedResource(t *testing.T) {
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", "moved"))
2024-06-27 10:08:08 -04:00
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
planReq := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, stacks_testing_provider.NewResourceStoreBuilder().
AddResource("moved", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})).
Build()), nil
},
},
2024-06-27 10:08:08 -04:00
DependencyLocks: *lock,
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: "testing_resource",
Name: "before",
},
Key: addrs.NoKey,
},
},
DeposedKey: addrs.NotDeposed,
},
}).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
SchemaVersion: 0,
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "moved",
"value": "moved",
}),
Status: states.ObjectReady,
}).
SetProviderAddr(addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
})).
Build(),
}
planChangesCh := make(chan stackplan.PlannedChange)
diagsCh := make(chan tfdiags.Diagnostic)
planResp := PlanResponse{
PlannedChanges: planChangesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &planReq, &planResp)
planChanges, diags := collectPlanOutput(planChangesCh, diagsCh)
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, go %s", diags.ErrWithWarnings())
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, stacks_testing_provider.NewResourceStoreBuilder().
AddResource("moved", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})).
Build()), nil
},
},
2024-06-27 10:08:08 -04:00
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
if len(applyDiags) > 0 {
t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings())
}
expectedPreviousAddr := mustAbsResourceInstanceObject("component.self.testing_resource.before")
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"),
PreviousResourceInstanceObjectAddr: &expectedPreviousAddr,
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "moved",
"value": "moved",
}),
Status: states.ObjectReady,
AttrSensitivePaths: make([]cty.Path, 0),
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyWithSensitivePropagation(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)
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{
stackaddrs.InputVariable{Name: "id"}: {
Value: cty.StringVal("bb5cf32312ec"),
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
planChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings())
}
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
if len(applyDiags) > 0 {
t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings())
}
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
Dependencies: collections.NewSet(mustAbsComponent("component.sensitive")),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("bb5cf32312ec"),
mustInputVariable("input"): cty.StringVal("secret").Mark(marks.Sensitive),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "bb5cf32312ec",
"value": "secret",
}),
AttrSensitivePaths: []cty.Path{
cty.GetAttrPath("value"),
},
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("testing"),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.sensitive"),
ComponentInstanceAddr: mustAbsComponentInstance("component.sensitive"),
Dependents: collections.NewSet(mustAbsComponent("component.self")),
OutputValues: map[addrs.OutputValue]cty.Value{
addrs.OutputValue{Name: "out"}: cty.StringVal("secret").Mark(marks.Sensitive),
},
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("id"),
Value: cty.StringVal("bb5cf32312ec"),
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
2024-06-21 08:05:26 -04:00
func TestApplyWithForcePlanTimestamp(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)
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
2024-06-21 08:05:26 -04:00
},
},
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
planChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings())
}
// Sanity check that the plan timestamp was set correctly
output := expectOutput(t, "plantimestamp", planChanges)
plantimestampValue := output.After
2024-06-21 08:05:26 -04:00
if plantimestampValue.AsString() != forcedPlanTimestamp {
t.Errorf("expected plantimestamp to be %q, got %q", forcedPlanTimestamp, plantimestampValue.AsString())
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
planLoader := stackplan.NewLoader()
2024-06-21 08:05:26 -04:00
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
2024-06-21 08:05:26 -04:00
}
applyReq := ApplyRequest{
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
Config: cfg,
Plan: plan,
2024-06-21 08:05:26 -04:00
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
2024-06-21 08:05:26 -04:00
},
},
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
if len(applyDiags) > 0 {
t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings())
}
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.second-self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.second-self"),
2024-06-21 08:05:26 -04:00
OutputValues: map[addrs.OutputValue]cty.Value{
// We want to make sure the plantimestamp is set correctly
{Name: "input"}: cty.StringVal(forcedPlanTimestamp),
// plantimestamp should also be set for the module runtime used in the components
{Name: "out"}: cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)),
},
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("value"): cty.StringVal(forcedPlanTimestamp),
},
2024-06-21 08:05:26 -04:00
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
2024-06-21 08:05:26 -04:00
OutputValues: map[addrs.OutputValue]cty.Value{
// We want to make sure the plantimestamp is set correctly
{Name: "input"}: cty.StringVal(forcedPlanTimestamp),
// plantimestamp should also be set for the module runtime used in the components
{Name: "out"}: cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)),
},
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("value"): cty.StringVal(forcedPlanTimestamp),
},
2024-06-21 08:05:26 -04:00
},
&stackstate.AppliedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "plantimestamp"},
Value: cty.StringVal(forcedPlanTimestamp),
},
2024-06-21 08:05:26 -04:00
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
2024-06-21 08:05:26 -04:00
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyWithDefaultPlanTimestamp(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)
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
2024-06-21 08:05:26 -04:00
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
planChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings())
}
// Sanity check that the plan timestamp was set correctly
output := expectOutput(t, "plantimestamp", planChanges)
plantimestampValue := output.After
2024-06-21 08:05:26 -04:00
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())
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
planLoader := stackplan.NewLoader()
2024-06-21 08:05:26 -04:00
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
2024-06-21 08:05:26 -04:00
}
applyReq := ApplyRequest{
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
Config: cfg,
Plan: plan,
2024-06-21 08:05:26 -04:00
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
2024-06-21 08:05:26 -04:00
},
},
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
if len(applyDiags) > 0 {
t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings())
}
for _, x := range applyChanges {
if v, ok := x.(*stackstate.AppliedChangeComponentInstance); ok {
if actualTimestampValue, ok := v.OutputValues[addrs.OutputValue{
Name: "input",
}]; ok {
actualTimestamp, err := time.Parse(time.RFC3339, actualTimestampValue.AsString())
if err != nil {
t.Fatalf("Could not parse component output value: %q", err)
}
if actualTimestamp.Before(dayOfWritingThisTestTime) {
t.Error("Timestamp is before day of writing this test, that should be incorrect.")
}
}
if actualTimestampValue, ok := v.OutputValues[addrs.OutputValue{
Name: "out",
}]; ok {
actualTimestamp, err := time.Parse(time.RFC3339, strings.ReplaceAll(actualTimestampValue.AsString(), "module-output-", ""))
if err != nil {
t.Fatalf("Could not parse component output value: %q", err)
}
if actualTimestamp.Before(dayOfWritingThisTestTime) {
t.Error("Timestamp is before day of writing this test, that should be incorrect.")
}
}
}
}
}
func TestApplyWithFailedComponent(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "failed-parent"))
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)
planChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings())
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
expectDiagnosticsForTest(t, applyDiags,
// This is the expected failure, from our testing_failed_resource.
expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"))
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.parent"),
ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
Dependents: collections.NewSet(mustAbsComponent("component.self")),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("input"): cty.StringVal("Hello, world!"),
mustInputVariable("id"): cty.NullVal(cty.String),
mustInputVariable("fail_plan"): cty.NullVal(cty.Bool),
mustInputVariable("fail_apply"): cty.BoolVal(true),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_failed_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
Dependencies: collections.NewSet(mustAbsComponent("component.parent")),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.NullVal(cty.String),
mustInputVariable("input"): cty.UnknownVal(cty.String),
},
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyWithFailedProviderLinkedComponent(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "failed-component-to-provider"))
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)
planChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings())
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
expectDiagnosticsForTest(t, applyDiags,
// This is the expected failure, from our testing_failed_resource.
expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"))
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.parent"),
ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
Dependents: collections.NewSet(mustAbsComponent("component.self")),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("input"): cty.NullVal(cty.String),
mustInputVariable("id"): cty.NullVal(cty.String),
mustInputVariable("fail_plan"): cty.NullVal(cty.Bool),
mustInputVariable("fail_apply"): cty.BoolVal(true),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_failed_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
Dependencies: collections.NewSet(mustAbsComponent("component.parent")),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.NullVal(cty.String),
mustInputVariable("input"): cty.StringVal("Hello, world!"),
},
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyWithStateManipulation(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 []stackstate.AppliedChange
counts collections.Map[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]
planDiags []expectedDiagnostic
applyDiags []expectedDiagnostic
}{
"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: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "moved",
"value": "moved",
}),
Status: states.ObjectReady,
AttrSensitivePaths: make([]cty.Path, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
PreviousResourceInstanceObjectAddr: mustAbsResourceInstanceObjectPtr("component.self.testing_resource.before"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
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,
},
}),
},
"moved-failed-dep": {
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: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "moved",
"value": "moved",
}),
Status: states.ObjectReady,
AttrSensitivePaths: make([]cty.Path, 0),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_failed_resource",
Name: "resource",
},
},
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
PreviousResourceInstanceObjectAddr: mustAbsResourceInstanceObjectPtr("component.self.testing_resource.before"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
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,
},
}),
applyDiags: []expectedDiagnostic{
// This error comes from the testing_failed_resource
expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"),
},
},
"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: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("imported"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "imported",
"value": "imported",
}),
Status: states.ObjectReady,
AttrSensitivePaths: make([]cty.Path, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("id"),
Value: cty.StringVal("imported"),
},
},
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,
},
}),
},
"import-failed-dep": {
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: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("imported"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "imported",
"value": "imported",
}),
Status: states.ObjectReady,
AttrSensitivePaths: make([]cty.Path, 0),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_failed_resource",
Name: "resource",
},
},
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("id"),
Value: cty.StringVal("imported"),
},
},
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,
},
}),
applyDiags: []expectedDiagnostic{
// This error comes from the testing_failed_resource
expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"),
},
},
"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: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"),
NewStateSrc: nil, // Deleted, so is nil.
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
},
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,
},
}),
planDiags: []expectedDiagnostic{
expectDiagnostic(tfdiags.Warning, "Some objects will no longer be managed by Terraform", "If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:\n - testing_resource.resource\n\nAfter applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again."),
},
},
"removed-failed-dep": {
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: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"),
NewStateSrc: nil, // Deleted, so is nil.
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
},
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,
},
}),
planDiags: []expectedDiagnostic{
expectDiagnostic(tfdiags.Warning, "Some objects will no longer be managed by Terraform", "If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:\n - testing_resource.resource\n\nAfter applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again."),
},
applyDiags: []expectedDiagnostic{
// This error comes from the testing_failed_resource
expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"),
},
},
"deferred": {
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("self", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("deferred"),
"value": cty.UnknownVal(cty.String),
})).
Build(),
changes: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.deferred"),
ComponentInstanceAddr: mustAbsComponentInstance("component.deferred"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.ok"),
ComponentInstanceAddr: mustAbsComponentInstance("component.ok"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.ok.testing_resource.self"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "ok",
"value": "ok",
}),
Status: states.ObjectReady,
AttrSensitivePaths: nil,
Dependencies: []addrs.ConfigResource{},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
PreviousResourceInstanceObjectAddr: nil,
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
K: mustAbsComponentInstance("component.ok"),
V: &hooks.ComponentInstanceChange{
Addr: mustAbsComponentInstance("component.ok"),
Add: 1,
Defer: 0,
},
},
collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
K: mustAbsComponentInstance("component.deferred"),
V: &hooks.ComponentInstanceChange{
Addr: mustAbsComponentInstance("component.deferred"),
Defer: 1,
},
},
),
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", name))
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
},
}
planChangeCh := make(chan stackplan.PlannedChange)
diagsCh := make(chan tfdiags.Diagnostic)
planReq := PlanRequest{
Config: cfg,
ProviderFactories: providers,
InputValues: inputs,
ForcePlanTimestamp: &fakePlanTimestamp,
PrevState: tc.state,
DependencyLocks: *lock,
}
planResp := PlanResponse{
PlannedChanges: planChangeCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &planReq, &planResp)
planChanges, diags := collectPlanOutput(planChangeCh, diagsCh)
sort.SliceStable(diags, diagnosticSortFunc(diags))
expectDiagnosticsForTest(t, diags, tc.planDiags...)
// Check the counts during the apply for this test.
gotCounts := collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]()
ctx = ContextWithHooks(ctx, &stackeval.Hooks{
ReportComponentInstanceApplied: func(ctx context.Context, span any, change *hooks.ComponentInstanceChange) any {
gotCounts.Put(change.Addr, change)
return span
},
})
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
Config: cfg,
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
Plan: plan,
ProviderFactories: providers,
DependencyLocks: *lock,
}
stacks+rpcapi: Load prior state and plan separately Previously we expected clients to provide an inline raw prior state to PlanStackChanges and an inline raw plan to ApplyStackChanges, which was a simpler design but meant that we might end up generating a state or plan that's too large to be submitted in a single gRPC request, which would then be difficult to resolve. Instead we'll offer separate RPC functions for loading raw state and plan using a gRPC streaming approach, which better mirrors the streaming approach we use to _emit_ these artifacts. Although we don't actually need this benefit right now, this makes it possible in principle for a client that's running PlanStackChanges to feed back the raw planned actions concurrently into OpenPlan and thus avoid buffering the whole plan on the client side at all. This required resolving the pre-existing FIXME about the inconsistency where stackeval wants a raw plan for apply but expects the caller to have dealt with loading the prior state for planning. Here it's resolved in the direction of the caller (rpcapi) always being responsible for loading both artifacts, because that means we can continue supporting the old inline approach for a while without that complexity having to infect the lower layers. Ideally we should remove the legacy approach before this API becomes constrained by compatibility promises, but I've preserved the old API for now to give us some flexibility in when we update the existing clients of this API to use the new approach. Co-authored-by: Martin Atkins <mart@degeneration.co.uk>
2024-07-12 22:20:34 -04:00
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, diags := collectApplyOutput(applyChangesCh, diagsCh)
sort.SliceStable(diags, diagnosticSortFunc(diags))
expectDiagnosticsForTest(t, diags, tc.applyDiags...)
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(tc.changes, applyChanges, 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 TestApplyWithChangedInputValues(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid"))
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{
stackaddrs.InputVariable{Name: "input"}: {
Value: cty.StringVal("hello"),
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
planChanges, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings())
}
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
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{
// This time we're deliberately changing the values we're giving
// to the apply operation. We expect this to fail earlier than
// the previous test.
stackaddrs.InputVariable{Name: "input"}: {
Value: cty.StringVal("world"),
},
},
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
sort.SliceStable(applyDiags, diagnosticSortFunc(applyDiags))
expectDiagnosticsForTest(t, applyDiags,
expectDiagnostic(
tfdiags.Error,
"Inconsistent value for input variable during apply",
"The value for non-ephemeral input variable \"input\" was set to a different value during apply than was set during plan. Only ephemeral input variables can change between the plan and apply phases."),
expectDiagnostic(tfdiags.Error, "Invalid inputs for component", "Input variable \"input\" could not be evaluated, additional diagnostics elsewhere should provide mode detail."),
)
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: make(map[addrs.InputVariable]cty.Value),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("id"),
Value: cty.NullVal(cty.String),
},
// no resources should have been created because the input variable was
// invalid.
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyAutomaticInputConversion(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "for-each-component"))
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{
stackaddrs.InputVariable{Name: "input"}: {
// The stack expects a map of strings, but we're giving it
// an object. Terraform should automatically convert this to
// the expected type.
Value: cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("hello"),
"world": cty.StringVal("world"),
}),
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
planChanges, planDiags := collectPlanOutput(changesCh, diagsCh)
if len(planDiags) > 0 {
t.Fatalf("expected no diagnostics, got %s", planDiags.ErrWithWarnings())
}
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
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{
stackaddrs.InputVariable{Name: "input"}: {
// The stack expects a map of strings, but we're giving it
// an object. Terraform should automatically convert this to
// the expected type.
Value: cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("hello"),
"world": cty.StringVal("world"),
}),
},
},
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
if len(applyDiags) > 0 {
t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings())
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"hello\"]"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("hello"),
mustInputVariable("input"): cty.StringVal("hello"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"hello\"].testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "hello",
"value": "hello",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"world\"]"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("world"),
mustInputVariable("input"): cty.StringVal("world"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"world\"].testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "world",
"value": "world",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("hello"),
"world": cty.StringVal("world"),
}),
},
}
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApply_DependsOnComponentWithNoInstances(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)
planRequest := 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!"),
},
},
}
planResponse := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &planRequest, &planResponse)
planChanges, planDiags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, planDiags)
if len(planDiags) != 0 {
t.FailNow()
}
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
_, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
reportDiagnosticsForTest(t, applyDiags)
if len(applyDiags) != 0 {
t.FailNow()
}
// don't care about the changes - just want to make sure that depends_on
// reference to a component with zero instances doesn't break anything
}
func TestApply_WithProviderFunctions(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-provider-functions"))
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)
planRequest := 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!"),
},
},
}
planResponse := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &planRequest, &planResponse)
planChanges, planDiags := collectPlanOutput(changesCh, diagsCh)
reportDiagnosticsForTest(t, planDiags)
if len(planDiags) != 0 {
t.FailNow()
}
sort.SliceStable(planChanges, func(i, j int) bool {
return plannedChangeSortKey(planChanges[i]) < plannedChangeSortKey(planChanges[j])
})
wantPlanChanges := []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
},
&stackplan.PlannedChangeComponentInstance{
Addr: mustAbsComponentInstance("component.self"),
PlanApplyable: true,
PlanComplete: true,
Action: plans.Create,
RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
PlannedInputValues: map[string]plans.DynamicValue{
"id": mustPlanDynamicValueDynamicType(cty.StringVal("2f9f3b84")),
"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
},
PlannedInputValueMarks: map[string][]cty.PathValueMarks{
"id": nil,
"input": nil,
},
PlannedOutputValues: map[string]cty.Value{
"value": cty.StringVal("hello, world!"),
},
PlannedCheckResults: &states.CheckResults{},
2025-05-08 11:42:05 -04:00
PlannedProviderFunctionResults: []lang.FunctionResultHash{
{
Key: providerFunctionHashArgs(mustDefaultRootProvider("testing").Provider, "echo", cty.StringVal("hello, world!")),
Result: providerFunctionHashResult(cty.StringVal("hello, world!")),
},
},
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.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("2f9f3b84"),
"value": cty.StringVal("hello, world!"),
})),
},
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackplan.PlannedChangeProviderFunctionResults{
2025-05-08 11:42:05 -04:00
Results: []lang.FunctionResultHash{
{
Key: providerFunctionHashArgs(mustDefaultRootProvider("testing").Provider, "echo", cty.StringVal("hello, world!")),
Result: providerFunctionHashResult(cty.StringVal("hello, world!")),
},
},
},
&stackplan.PlannedChangeHeader{
TerraformVersion: version.SemVer,
},
&stackplan.PlannedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "value"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("hello, world!"),
},
&stackplan.PlannedChangePlannedTimestamp{
PlannedTimestamp: fakePlanTimestamp,
},
&stackplan.PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "input"},
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.StringVal("hello, world!"),
},
}
if diff := cmp.Diff(wantPlanChanges, planChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
// just verify the plan is correctly loading the provider function results
// as well
if len(plan.FunctionResults) == 0 {
t.Errorf("expected provider function results, got none")
if len(plan.GetComponent(mustAbsComponentInstance("component.self")).PlannedFunctionResults) == 0 {
t.Errorf("expected component function results, got none")
}
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
diagsCh = make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
reportDiagnosticsForTest(t, applyDiags)
if len(applyDiags) != 0 {
t.FailNow()
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
wantApplyChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: map[addrs.OutputValue]cty.Value{
{Name: "value"}: cty.StringVal("hello, world!"),
},
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("2f9f3b84"),
mustInputVariable("input"): cty.StringVal("hello, world!"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "2f9f3b84",
"value": "hello, world!",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
ProviderConfigAddr: mustDefaultRootProvider("testing"),
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeOutputValue{
Addr: stackaddrs.OutputValue{Name: "value"},
Value: cty.StringVal("hello, world!"),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.StringVal("hello, world!"),
},
}
2024-08-22 09:40:47 -04:00
if diff := cmp.Diff(wantApplyChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyFailedDependencyWithResourceInState(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "failed-dependency")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z")
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{}),
)
store := stacks_testing_provider.NewResourceStoreBuilder().
AddResource("resource", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("resource"),
"value": cty.NullVal(cty.String),
})).
Build()
planReq := PlanRequest{
PlanMode: plans.NormalMode,
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, store), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
stackaddrs.InputVariable{Name: "fail_apply"}: {
Value: cty.True,
},
},
// We have a resource in the state from a previous run. We shouldn't
// emit any state changes to this resource as a result of the dependency
// failing.
PrevState: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
SchemaVersion: 0,
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "resource",
"value": nil,
}),
Status: states.ObjectReady,
})).
Build(),
}
planChangesCh := make(chan stackplan.PlannedChange)
planDiagsCh := make(chan tfdiags.Diagnostic)
planResp := PlanResponse{
PlannedChanges: planChangesCh,
Diagnostics: planDiagsCh,
}
go Plan(ctx, &planReq, &planResp)
planChanges, planDiags := collectPlanOutput(planChangesCh, planDiagsCh)
if len(planDiags) > 0 {
t.Fatalf("unexpected diagnostics during planning: %s", planDiags)
}
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, store), nil
},
},
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
applyDiagsCh := make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: applyDiagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, applyDiagsCh)
expectDiagnosticsForTest(t, applyDiags, expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"))
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("resource_id"): cty.StringVal("resource"),
mustInputVariable("failed_id"): cty.StringVal("failed"),
mustInputVariable("fail_apply"): cty.True,
mustInputVariable("fail_plan"): cty.False,
mustInputVariable("input"): cty.NullVal(cty.String),
2024-09-11 04:58:09 -04:00
},
},
&stackstate.AppliedChangeResourceInstanceObject{
// This has no state as the apply operation failed and it wasn't
// in the state before.
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
&stackstate.AppliedChangeResourceInstanceObject{
// This emits the state from the previous run, as it was not
// changed during this run.
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "resource",
"value": nil,
}),
AttrSensitivePaths: make([]cty.Path, 0),
Status: states.ObjectReady,
Dependencies: []addrs.ConfigResource{mustAbsResourceInstance("testing_failed_resource.data").ConfigResource()},
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("fail_apply"),
Value: cty.True,
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("fail_plan"),
Value: cty.False,
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyManuallyRemovedResource(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid"))
fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z")
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{}),
)
planReq := PlanRequest{
PlanMode: plans.NormalMode,
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{
stackaddrs.InputVariable{Name: "id"}: {
Value: cty.StringVal("foo"),
},
stackaddrs.InputVariable{Name: "input"}: {
Value: cty.StringVal("hello"),
},
},
// We have in the previous state a resource that is not in our
// underlying data store. This simulates the case where someone went
// in and manually deleted a resource that Terraform is managing.
//
// Some providers will return an error in this case, but some will
// not. We need to ensure that we handle the second case gracefully.
PrevState: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.missing")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
SchemaVersion: 0,
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "e84b59f2",
"value": "hello",
}),
Status: states.ObjectReady,
})).
Build(),
}
planChangesCh := make(chan stackplan.PlannedChange)
planDiagsCh := make(chan tfdiags.Diagnostic)
planResp := PlanResponse{
PlannedChanges: planChangesCh,
Diagnostics: planDiagsCh,
}
go Plan(ctx, &planReq, &planResp)
planChanges, planDiags := collectPlanOutput(planChangesCh, planDiagsCh)
if len(planDiags) > 0 {
t.Fatalf("unexpected diagnostics during planning: %s", planDiags)
}
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
applyDiagsCh := make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: applyDiagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, applyDiagsCh)
if len(applyDiags) > 0 {
t.Fatalf("unexpected diagnostics during apply: %s", applyDiags)
}
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
InputVariables: map[addrs.InputVariable]cty.Value{
mustInputVariable("id"): cty.StringVal("foo"),
mustInputVariable("input"): cty.StringVal("hello"),
},
},
// The resource in our configuration has been updated, so that is
// present as normal.
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: &states.ResourceInstanceObjectSrc{
AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
"id": "foo",
"value": "hello",
}),
Status: states.ObjectReady,
Dependencies: make([]addrs.ConfigResource, 0),
},
Schema: stacks_testing_provider.TestingResourceSchema,
},
// The resource that was in state but not in the configuration should
// be removed from state.
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.missing"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
NewStateSrc: nil, // We should be removing this from the state file.
Schema: providers.Schema{},
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("id"),
Value: cty.StringVal("foo"),
},
&stackstate.AppliedChangeInputVariable{
Addr: mustStackInputVariable("input"),
Value: cty.StringVal("hello"),
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func collectApplyOutput(changesCh <-chan stackstate.AppliedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackstate.AppliedChange, tfdiags.Diagnostics) {
var changes []stackstate.AppliedChange
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)
}
}
}