mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-03 14:00:52 -05:00
387 lines
11 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|