mirror of https://github.com/hashicorp/terraform
stacks: include resources in state when calculating required providers (#34645)
* stacks: include resources in state when calculating required providers * also support apply time * add copywrite headerspull/34682/head
parent
67d5d8f79f
commit
07f6621091
@ -0,0 +1,244 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package stackruntime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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/providers"
|
||||
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
|
||||
"github.com/hashicorp/terraform/internal/stacks/stackplan"
|
||||
"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)
|
||||
}
|
||||
|
||||
attrs := map[string]interface{}{
|
||||
"id": "FE1D5830765C",
|
||||
"input": map[string]interface{}{
|
||||
"value": "hello",
|
||||
"type": "string",
|
||||
},
|
||||
"output": map[string]interface{}{
|
||||
"value": nil,
|
||||
"type": "string",
|
||||
},
|
||||
"triggers_replace": nil,
|
||||
}
|
||||
attrsJSON, err := json.Marshal(attrs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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: attrsJSON,
|
||||
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{
|
||||
Complete: false,
|
||||
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())
|
||||
}
|
||||
|
||||
if len(applyChanges) != 2 {
|
||||
t.Fatalf("expected 2 applied changes, got %d", len(applyChanges))
|
||||
}
|
||||
|
||||
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 {
|
||||
// An arbitrary sort just to make the result stable for comparison.
|
||||
return fmt.Sprintf("%T", applyChanges[i]) < fmt.Sprintf("%T", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
terraform = {
|
||||
source = "terraform.io/builtin/terraform"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
required_providers {
|
||||
terraform = {
|
||||
source = "terraform.io/builtin/terraform"
|
||||
}
|
||||
}
|
||||
|
||||
provider "terraform" "default" {}
|
||||
|
||||
component "self" {
|
||||
source = "../"
|
||||
|
||||
providers = {}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
required_providers {
|
||||
terraform = {
|
||||
source = "terraform.io/builtin/terraform"
|
||||
}
|
||||
}
|
||||
|
||||
provider "terraform" "default" {}
|
||||
|
||||
component "self" {
|
||||
source = "../"
|
||||
|
||||
providers = {
|
||||
terraform = provider.terraform.default
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
terraform = {
|
||||
source = "terraform.io/builtin/terraform"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "terraform_data" "main" {
|
||||
input = "hello"
|
||||
}
|
||||
|
||||
output "input" {
|
||||
value = terraform_data.main.input
|
||||
}
|
||||
|
||||
output "output" {
|
||||
value = terraform_data.main.output
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
required_providers {
|
||||
terraform = {
|
||||
source = "terraform.io/builtin/terraform"
|
||||
}
|
||||
}
|
||||
|
||||
provider "terraform" "default" {
|
||||
}
|
||||
|
||||
component "self" {
|
||||
source = "./"
|
||||
|
||||
providers = {}
|
||||
}
|
||||
|
||||
output "obj" {
|
||||
type = object({
|
||||
input = string
|
||||
output = string
|
||||
})
|
||||
value = component.self
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package stackstate
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
)
|
||||
|
||||
// StateBuilder wraps State, and provides some write-only methods to update the
|
||||
// state.
|
||||
//
|
||||
// This is generally used to build up a new state from scratch during tests.
|
||||
type StateBuilder struct {
|
||||
state *State
|
||||
}
|
||||
|
||||
func NewStateBuilder() *StateBuilder {
|
||||
return &StateBuilder{
|
||||
state: NewState(),
|
||||
}
|
||||
}
|
||||
|
||||
// Build returns the state and invalidates the StateBuilder.
|
||||
//
|
||||
// You will get nil pointer exceptions if you attempt to use the builder after
|
||||
// calling Build.
|
||||
func (s *StateBuilder) Build() *State {
|
||||
ret := s.state
|
||||
s.state = nil
|
||||
return ret
|
||||
}
|
||||
|
||||
// AddResourceInstance adds a resource instance to the state.
|
||||
func (s *StateBuilder) AddResourceInstance(builder *ResourceInstanceBuilder) *StateBuilder {
|
||||
if builder.addr == nil || builder.src == nil || builder.providerAddr == nil {
|
||||
panic("ResourceInstanceBuilder is missing required fields")
|
||||
}
|
||||
s.state.addResourceInstanceObject(*builder.addr, builder.src, *builder.providerAddr)
|
||||
return s
|
||||
}
|
||||
|
||||
type ResourceInstanceBuilder struct {
|
||||
addr *stackaddrs.AbsResourceInstanceObject
|
||||
src *states.ResourceInstanceObjectSrc
|
||||
providerAddr *addrs.AbsProviderConfig
|
||||
}
|
||||
|
||||
func NewResourceInstanceBuilder() *ResourceInstanceBuilder {
|
||||
return &ResourceInstanceBuilder{}
|
||||
}
|
||||
|
||||
func (b *ResourceInstanceBuilder) SetAddr(addr stackaddrs.AbsResourceInstanceObject) *ResourceInstanceBuilder {
|
||||
b.addr = &addr
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *ResourceInstanceBuilder) SetResourceInstanceObjectSrc(src states.ResourceInstanceObjectSrc) *ResourceInstanceBuilder {
|
||||
b.src = &src
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *ResourceInstanceBuilder) SetProviderAddr(addr addrs.AbsProviderConfig) *ResourceInstanceBuilder {
|
||||
b.providerAddr = &addr
|
||||
return b
|
||||
}
|
||||
Loading…
Reference in new issue