You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/stacks/stackruntime/apply_test.go

1737 lines
52 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackruntime
import (
"context"
"fmt"
"path"
"path/filepath"
"sort"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/terraform/internal/addrs"
terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"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"
)
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"))
lock := depsfile.NewLocks()
planReq := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
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())
}
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.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: stackaddrs.AbsComponent{
Item: stackaddrs.Component{
Name: "self",
},
},
ComponentInstanceAddr: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
},
},
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.AbsResourceInstance{
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "terraform_data",
Name: "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, ctydebug.CmpOptions, cmpCollectionsSet); 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"))
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(stacks_testing_provider.NewResourceStoreBuilder().
AddResource("moved", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})).
Build()), nil
},
},
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())
}
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(stacks_testing_provider.NewResourceStoreBuilder().
AddResource("moved", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})).
Build()), 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())
}
expectedPreviousAddr := mustAbsResourceInstanceObject("component.self.testing_resource.before")
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: stackaddrs.AbsComponent{
Item: stackaddrs.Component{
Name: "self",
},
},
ComponentInstanceAddr: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
},
},
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.AbsResourceInstance{
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "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, ctydebug.CmpOptions, cmpCollectionsSet); 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(), 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(), 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: stackaddrs.AbsComponent{
Item: stackaddrs.Component{
Name: "self",
},
},
ComponentInstanceAddr: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
},
},
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.AbsResourceInstance{
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "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: stackaddrs.AbsComponent{
Item: stackaddrs.Component{
Name: "sensitive",
},
},
ComponentInstanceAddr: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "sensitive",
},
},
},
OutputValues: map[addrs.OutputValue]cty.Value{
addrs.OutputValue{Name: "out"}: cty.StringVal("secret").Mark(marks.Sensitive),
},
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
func TestApplyWithCheckableObjects(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "checkable-objects")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
store := stacks_testing_provider.NewResourceStore()
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.NewProviderWithData(store), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
stackaddrs.InputVariable{Name: "foo"}: {
Value: cty.StringVal("bar"),
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
var wantDiags tfdiags.Diagnostics
wantDiags = wantDiags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Check block assertion failed",
Detail: `value must be 'baz'`,
Subject: &hcl.Range{
Filename: mainBundleSourceAddrStr("checkable-objects/checkable-objects.tf"),
Start: hcl.Pos{Line: 41, Column: 21, Byte: 716},
End: hcl.Pos{Line: 41, Column: 57, Byte: 752},
},
})
go Plan(ctx, &req, &resp)
planChanges, planDiags := collectPlanOutput(changesCh, diagsCh)
if diff := cmp.Diff(wantDiags.ForRPC(), planDiags.ForRPC()); diff != "" {
t.Errorf("wrong diagnostics\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)
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(store), 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 diff := cmp.Diff(wantDiags.ForRPC(), applyDiags.ForRPC()); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: stackaddrs.AbsComponent{
Item: stackaddrs.Component{
Name: "single",
},
},
ComponentInstanceAddr: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "single",
},
},
},
OutputValues: map[addrs.OutputValue]cty.Value{
addrs.OutputValue{Name: "foo"}: cty.StringVal("bar"),
},
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "single",
},
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.AbsResourceInstance{
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "testing_resource",
Name: "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,
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
// capture the state
state := make(map[string]*anypb.Any)
for _, change := range applyChanges {
proto, err := change.AppliedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, raw := range proto.Raw {
state[raw.Key] = raw.Value
}
}
prevState, err := stackstate.LoadFromProto(state)
if err != nil {
t.Fatalf("failed to load state from proto: %s", err)
}
// We'll follow this up with a destroy plan to verify everything the checks
// don't get in the way here.
changesCh = make(chan stackplan.PlannedChange)
diagsCh = make(chan tfdiags.Diagnostic)
req = PlanRequest{
// For this plan, we're destroying and we now have some state.
PlanMode: plans.DestroyMode,
PrevState: prevState,
// The rest is the same as the previous plan.
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(store), nil
},
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
stackaddrs.InputVariable{Name: "foo"}: {
Value: cty.StringVal("bar"),
},
},
}
resp = PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
planChanges, planDiags = collectPlanOutput(changesCh, diagsCh)
if len(planDiags) > 0 {
// At this point we shouldn't see the check warning, as they don't
// execute during destroy plans.
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)
}
// And now we'll apply the destroy plan to verify that the checks don't
// get in the way here.
applyReq = ApplyRequest{
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(store), 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 {
// Again, the warning from the check block shouldn't be included during
// a destroy operation.
t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings())
}
wantChanges = []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.single"),
ComponentInstanceAddr: mustAbsComponentInstance("component.single"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.main"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
}
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(), nil
},
},
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, err := output.NewValue.Decode(cty.String)
if err != nil {
t.Fatal(err)
}
if plantimestampValue.AsString() != forcedPlanTimestamp {
t.Errorf("expected plantimestamp to be %q, got %q", forcedPlanTimestamp, plantimestampValue.AsString())
}
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(), 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: stackaddrs.AbsComponent{
Item: stackaddrs.Component{
Name: "second-self",
},
},
ComponentInstanceAddr: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "second-self",
},
},
},
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)),
},
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: stackaddrs.AbsComponent{
Item: stackaddrs.Component{
Name: "self",
},
},
ComponentInstanceAddr: stackaddrs.AbsComponentInstance{
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
},
},
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)),
},
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" {
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(), nil
},
},
}
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, err := output.NewValue.Decode(cty.String)
if err != nil {
t.Fatal(err)
}
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())
}
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(), 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())
}
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(), 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())
}
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(), 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, "planned failure", "apply failure"))
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.parent"),
ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_failed_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); 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(), 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())
}
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(), 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, "planned failure", "apply failure"))
wantChanges := []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.parent"),
ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
&stackstate.AppliedChangeResourceInstanceObject{
ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_failed_resource.data"),
ProviderConfigAddr: mustDefaultRootProvider("testing"),
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]cty.Value),
},
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(wantChanges, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); 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]
expectedWarnings []string
}{
"moved": {
state: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "moved",
"value": "moved",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("moved", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("moved"),
"value": cty.StringVal("moved"),
})).
Build(),
changes: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]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,
},
}),
},
"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),
},
&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,
},
},
counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
K: mustAbsComponentInstance("component.self"),
V: &hooks.ComponentInstanceChange{
Addr: mustAbsComponentInstance("component.self"),
Import: 1,
},
}),
},
"removed": {
state: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.resource")).
SetProviderAddr(mustDefaultRootProvider("testing")).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustMarshalJSONAttrs(map[string]any{
"id": "removed",
"value": "removed",
}),
})).
Build(),
store: stacks_testing_provider.NewResourceStoreBuilder().
AddResource("removed", cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("removed"),
"value": cty.StringVal("removed"),
})).
Build(),
changes: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
OutputValues: make(map[addrs.OutputValue]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,
},
}),
expectedWarnings: []string{"Some objects will no longer be managed by Terraform"},
},
"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),
},
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.ok"),
ComponentInstanceAddr: mustAbsComponentInstance("component.ok"),
OutputValues: make(map[addrs.OutputValue]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(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)
reportDiagnosticsForTest(t, diags)
if diags.HasErrors() {
// we reported the diagnostics above, so we can just fail now
t.FailNow()
}
if len(diags) > len(tc.expectedWarnings) {
t.Fatalf("had unexpected warnings")
}
for i, diag := range diags {
if diag.Description().Summary != tc.expectedWarnings[i] {
t.Fatalf("expected diagnostic with summary %q, got %q", tc.expectedWarnings[i], diag.Description().Summary)
}
}
// 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
},
})
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: providers,
DependencyLocks: *lock,
}
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)
reportDiagnosticsForTest(t, diags)
if diags.HasErrors() {
// we reported the diagnostics above, so we can just fail now
t.FailNow()
}
if len(diags) > 0 {
t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings())
}
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
})
if diff := cmp.Diff(tc.changes, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" {
t.Errorf("wrong changes\n%s", diff)
}
wantCounts := tc.counts
for _, elem := range wantCounts.Elems() {
// First, make sure everything we wanted is present.
if !gotCounts.HasKey(elem.K) {
t.Errorf("wrong counts: wanted %s but didn't get it", elem.K)
}
// And that the values actually match.
got, want := gotCounts.Get(elem.K), elem.V
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong counts for %s: %s", want.Addr, diff)
}
}
for _, elem := range gotCounts.Elems() {
// Then, make sure we didn't get anything we didn't want.
if !wantCounts.HasKey(elem.K) {
t.Errorf("wrong counts: got %s but didn't want it", elem.K)
}
}
})
}
}
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)
}
}
}