terraform/internal/stacks/stackruntime/apply_test.go
2024-02-21 10:40:20 +01:00

387 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackruntime
import (
"context"
"path"
"sort"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"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/lang/marks"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
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"))
planReq := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
ForcePlanTimestamp: &fakePlanTimestamp,
// PrevState specifies a state with a resource that is not present in
// the current configuration. This is a common situation when a resource
// is removed from the configuration but still exists in the state.
PrevState: stackstate.NewStateBuilder().
AddResourceInstance(stackstate.NewResourceInstanceBuilder().
SetAddr(stackaddrs.AbsResourceInstanceObject{
Component: stackaddrs.AbsComponentInstance{
Stack: stackaddrs.RootStackInstance,
Item: stackaddrs.ComponentInstance{
Component: stackaddrs.Component{
Name: "self",
},
Key: addrs.NoKey,
},
},
Item: addrs.AbsResourceInstanceObject{
ResourceInstance: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "terraform_data",
Name: "main",
},
Key: addrs.NoKey,
},
},
DeposedKey: addrs.NotDeposed,
},
}).
SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
SchemaVersion: 0,
AttrsJSON: 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())
}
var raw []*anypb.Any
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
raw = append(raw, proto.Raw...)
}
applyReq := ApplyRequest{
Config: cfg,
RawPlan: raw,
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 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)
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,
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())
}
var raw []*anypb.Any
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
raw = append(raw, proto.Raw...)
}
applyReq := ApplyRequest{
Config: cfg,
RawPlan: raw,
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: "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.PathValueMarks{
{
Path: cty.GetAttrPath("value"),
Marks: cty.NewValueMarks(marks.Sensitive),
},
},
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 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)
}
}
}