mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-18 12:42:58 -06:00
f0ee0ef5cc
When an output change was decoded, we were decoding the raw value rather than the ChangeSrc, which lost any encoded marks for that value.
510 lines
14 KiB
Go
510 lines
14 KiB
Go
package terraform
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/providers"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// Test that the PreApply hook is called with the correct deposed key
|
|
func TestContext2Apply_createBeforeDestroy_deposedKeyPreApply(t *testing.T) {
|
|
m := testModule(t, "apply-cbd-deposed-only")
|
|
p := testProvider("aws")
|
|
p.PlanResourceChangeFn = testDiffFn
|
|
p.ApplyResourceChangeFn = testApplyFn
|
|
|
|
deposedKey := states.NewDeposedKey()
|
|
|
|
state := states.NewState()
|
|
root := state.EnsureModule(addrs.RootModuleInstance)
|
|
root.SetResourceInstanceCurrent(
|
|
mustResourceInstanceAddr("aws_instance.bar").Resource,
|
|
&states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: []byte(`{"id":"bar"}`),
|
|
},
|
|
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
|
|
)
|
|
root.SetResourceInstanceDeposed(
|
|
mustResourceInstanceAddr("aws_instance.bar").Resource,
|
|
deposedKey,
|
|
&states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectTainted,
|
|
AttrsJSON: []byte(`{"id":"foo"}`),
|
|
},
|
|
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
|
|
)
|
|
|
|
hook := new(MockHook)
|
|
ctx := testContext2(t, &ContextOpts{
|
|
Config: m,
|
|
Hooks: []Hook{hook},
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
|
},
|
|
State: state,
|
|
})
|
|
|
|
if p, diags := ctx.Plan(); diags.HasErrors() {
|
|
t.Fatalf("diags: %s", diags.Err())
|
|
} else {
|
|
t.Logf(legacyDiffComparisonString(p.Changes))
|
|
}
|
|
|
|
state, diags := ctx.Apply()
|
|
if diags.HasErrors() {
|
|
t.Fatalf("diags: %s", diags.Err())
|
|
}
|
|
|
|
// Verify PreApply was called correctly
|
|
if !hook.PreApplyCalled {
|
|
t.Fatalf("PreApply hook not called")
|
|
}
|
|
if addr, wantAddr := hook.PreApplyAddr, mustResourceInstanceAddr("aws_instance.bar"); !addr.Equal(wantAddr) {
|
|
t.Errorf("expected addr to be %s, but was %s", wantAddr, addr)
|
|
}
|
|
if gen := hook.PreApplyGen; gen != deposedKey {
|
|
t.Errorf("expected gen to be %q, but was %q", deposedKey, gen)
|
|
}
|
|
}
|
|
|
|
func TestContext2Apply_destroyWithDataSourceExpansion(t *testing.T) {
|
|
// While managed resources store their destroy-time dependencies, data
|
|
// sources do not. This means that if a provider were only included in a
|
|
// destroy graph because of data sources, it could have dependencies which
|
|
// are not correctly ordered. Here we verify that the provider is not
|
|
// included in the destroy operation, and all dependency evaluations
|
|
// succeed.
|
|
|
|
m := testModuleInline(t, map[string]string{
|
|
"main.tf": `
|
|
module "mod" {
|
|
source = "./mod"
|
|
}
|
|
|
|
provider "other" {
|
|
foo = module.mod.data
|
|
}
|
|
|
|
# this should not require the provider be present during destroy
|
|
data "other_data_source" "a" {
|
|
}
|
|
`,
|
|
|
|
"mod/main.tf": `
|
|
data "test_data_source" "a" {
|
|
count = 1
|
|
}
|
|
|
|
data "test_data_source" "b" {
|
|
count = data.test_data_source.a[0].foo == "ok" ? 1 : 0
|
|
}
|
|
|
|
output "data" {
|
|
value = data.test_data_source.a[0].foo == "ok" ? data.test_data_source.b[0].foo : "nope"
|
|
}
|
|
`,
|
|
})
|
|
|
|
testP := testProvider("test")
|
|
otherP := testProvider("other")
|
|
|
|
readData := func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
|
|
return providers.ReadDataSourceResponse{
|
|
State: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("data_source"),
|
|
"foo": cty.StringVal("ok"),
|
|
}),
|
|
}
|
|
}
|
|
|
|
testP.ReadDataSourceFn = readData
|
|
otherP.ReadDataSourceFn = readData
|
|
|
|
ps := map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP),
|
|
addrs.NewDefaultProvider("other"): testProviderFuncFixed(otherP),
|
|
}
|
|
|
|
otherP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
|
|
foo := req.Config.GetAttr("foo")
|
|
if foo.IsNull() || foo.AsString() != "ok" {
|
|
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect config val: %#v\n", foo))
|
|
}
|
|
return resp
|
|
}
|
|
|
|
ctx := testContext2(t, &ContextOpts{
|
|
Config: m,
|
|
Providers: ps,
|
|
})
|
|
|
|
_, diags := ctx.Plan()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
_, diags = ctx.Apply()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
// now destroy the whole thing
|
|
ctx = testContext2(t, &ContextOpts{
|
|
Config: m,
|
|
Providers: ps,
|
|
PlanMode: plans.DestroyMode,
|
|
})
|
|
|
|
_, diags = ctx.Plan()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
otherP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
|
|
// should not be used to destroy data sources
|
|
resp.Diagnostics = resp.Diagnostics.Append(errors.New("provider should not be used"))
|
|
return resp
|
|
}
|
|
|
|
_, diags = ctx.Apply()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
}
|
|
|
|
func TestContext2Apply_destroyThenUpdate(t *testing.T) {
|
|
m := testModuleInline(t, map[string]string{
|
|
"main.tf": `
|
|
resource "test_instance" "a" {
|
|
value = "udpated"
|
|
}
|
|
`,
|
|
})
|
|
|
|
p := testProvider("test")
|
|
p.PlanResourceChangeFn = testDiffFn
|
|
|
|
var orderMu sync.Mutex
|
|
var order []string
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
|
|
id := req.PriorState.GetAttr("id").AsString()
|
|
if id == "b" {
|
|
// slow down the b destroy, since a should wait for it
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
orderMu.Lock()
|
|
order = append(order, id)
|
|
orderMu.Unlock()
|
|
|
|
resp.NewState = req.PlannedState
|
|
return resp
|
|
}
|
|
|
|
addrA := mustResourceInstanceAddr(`test_instance.a`)
|
|
addrB := mustResourceInstanceAddr(`test_instance.b`)
|
|
|
|
state := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"a","value":"old","type":"test"}`),
|
|
Status: states.ObjectReady,
|
|
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
|
|
|
|
// test_instance.b depended on test_instance.a, and therefor should be
|
|
// destroyed before any changes to test_instance.a
|
|
s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"b"}`),
|
|
Status: states.ObjectReady,
|
|
Dependencies: []addrs.ConfigResource{addrA.ContainingResource().Config()},
|
|
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
|
|
})
|
|
|
|
ctx := testContext2(t, &ContextOpts{
|
|
Config: m,
|
|
State: state,
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
|
},
|
|
})
|
|
|
|
if _, diags := ctx.Plan(); diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
_, diags := ctx.Apply()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
if order[0] != "b" {
|
|
t.Fatalf("expected apply order [b, a], got: %v\n", order)
|
|
}
|
|
}
|
|
|
|
// verify that dependencies are updated in the state during refresh and apply
|
|
func TestApply_updateDependencies(t *testing.T) {
|
|
state := states.NewState()
|
|
root := state.EnsureModule(addrs.RootModuleInstance)
|
|
|
|
fooAddr := mustResourceInstanceAddr("aws_instance.foo")
|
|
barAddr := mustResourceInstanceAddr("aws_instance.bar")
|
|
bazAddr := mustResourceInstanceAddr("aws_instance.baz")
|
|
bamAddr := mustResourceInstanceAddr("aws_instance.bam")
|
|
binAddr := mustResourceInstanceAddr("aws_instance.bin")
|
|
root.SetResourceInstanceCurrent(
|
|
fooAddr.Resource,
|
|
&states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: []byte(`{"id":"foo"}`),
|
|
Dependencies: []addrs.ConfigResource{
|
|
bazAddr.ContainingResource().Config(),
|
|
},
|
|
},
|
|
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
|
|
)
|
|
root.SetResourceInstanceCurrent(
|
|
binAddr.Resource,
|
|
&states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: []byte(`{"id":"bin","type":"aws_instance","unknown":"ok"}`),
|
|
Dependencies: []addrs.ConfigResource{
|
|
bazAddr.ContainingResource().Config(),
|
|
},
|
|
},
|
|
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
|
|
)
|
|
root.SetResourceInstanceCurrent(
|
|
bazAddr.Resource,
|
|
&states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: []byte(`{"id":"baz"}`),
|
|
Dependencies: []addrs.ConfigResource{
|
|
// Existing dependencies should not be removed from orphaned instances
|
|
bamAddr.ContainingResource().Config(),
|
|
},
|
|
},
|
|
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
|
|
)
|
|
root.SetResourceInstanceCurrent(
|
|
barAddr.Resource,
|
|
&states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: []byte(`{"id":"bar","foo":"foo"}`),
|
|
},
|
|
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
|
|
)
|
|
|
|
m := testModuleInline(t, map[string]string{
|
|
"main.tf": `
|
|
resource "aws_instance" "bar" {
|
|
foo = aws_instance.foo.id
|
|
}
|
|
|
|
resource "aws_instance" "foo" {
|
|
}
|
|
|
|
resource "aws_instance" "bin" {
|
|
}
|
|
`,
|
|
})
|
|
|
|
p := testProvider("aws")
|
|
|
|
ctx := testContext2(t, &ContextOpts{
|
|
Config: m,
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
|
},
|
|
State: state,
|
|
})
|
|
|
|
plan, diags := ctx.Plan()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
bar := plan.PriorState.ResourceInstance(barAddr)
|
|
if len(bar.Current.Dependencies) == 0 || !bar.Current.Dependencies[0].Equal(fooAddr.ContainingResource().Config()) {
|
|
t.Fatalf("bar should depend on foo after refresh, but got %s", bar.Current.Dependencies)
|
|
}
|
|
|
|
foo := plan.PriorState.ResourceInstance(fooAddr)
|
|
if len(foo.Current.Dependencies) == 0 || !foo.Current.Dependencies[0].Equal(bazAddr.ContainingResource().Config()) {
|
|
t.Fatalf("foo should depend on baz after refresh because of the update, but got %s", foo.Current.Dependencies)
|
|
}
|
|
|
|
bin := plan.PriorState.ResourceInstance(binAddr)
|
|
if len(bin.Current.Dependencies) != 0 {
|
|
t.Fatalf("bin should depend on nothing after refresh because there is no change, but got %s", bin.Current.Dependencies)
|
|
}
|
|
|
|
baz := plan.PriorState.ResourceInstance(bazAddr)
|
|
if len(baz.Current.Dependencies) == 0 || !baz.Current.Dependencies[0].Equal(bamAddr.ContainingResource().Config()) {
|
|
t.Fatalf("baz should depend on bam after refresh, but got %s", baz.Current.Dependencies)
|
|
}
|
|
|
|
state, diags = ctx.Apply()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
bar = state.ResourceInstance(barAddr)
|
|
if len(bar.Current.Dependencies) == 0 || !bar.Current.Dependencies[0].Equal(fooAddr.ContainingResource().Config()) {
|
|
t.Fatalf("bar should still depend on foo after apply, but got %s", bar.Current.Dependencies)
|
|
}
|
|
|
|
foo = state.ResourceInstance(fooAddr)
|
|
if len(foo.Current.Dependencies) != 0 {
|
|
t.Fatalf("foo should have no deps after apply, but got %s", foo.Current.Dependencies)
|
|
}
|
|
|
|
}
|
|
|
|
func TestContext2Apply_additionalSensitiveFromState(t *testing.T) {
|
|
// Ensure we're not trying to double-mark values decoded from state
|
|
m := testModuleInline(t, map[string]string{
|
|
"main.tf": `
|
|
variable "secret" {
|
|
sensitive = true
|
|
default = ["secret"]
|
|
}
|
|
|
|
resource "test_resource" "a" {
|
|
sensitive_attr = var.secret
|
|
}
|
|
|
|
resource "test_resource" "b" {
|
|
value = test_resource.a.id
|
|
}
|
|
`,
|
|
})
|
|
|
|
p := new(MockProvider)
|
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
|
ResourceTypes: map[string]*configschema.Block{
|
|
"test_resource": {
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {
|
|
Type: cty.String,
|
|
Computed: true,
|
|
},
|
|
"value": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
"sensitive_attr": {
|
|
Type: cty.List(cty.String),
|
|
Optional: true,
|
|
Sensitive: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
state := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
mustResourceInstanceAddr(`test_resource.a`),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"a","sensitive_attr":["secret"]}`),
|
|
AttrSensitivePaths: []cty.PathValueMarks{
|
|
{
|
|
Path: cty.GetAttrPath("sensitive_attr"),
|
|
Marks: cty.NewValueMarks("sensitive"),
|
|
},
|
|
},
|
|
Status: states.ObjectReady,
|
|
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
|
)
|
|
})
|
|
|
|
ctx := testContext2(t, &ContextOpts{
|
|
Config: m,
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
|
},
|
|
State: state,
|
|
})
|
|
|
|
_, diags := ctx.Plan()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.ErrWithWarnings())
|
|
}
|
|
|
|
_, diags = ctx.Apply()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.ErrWithWarnings())
|
|
}
|
|
}
|
|
|
|
func TestContext2Apply_sensitiveOutputPassthrough(t *testing.T) {
|
|
// Ensure we're not trying to double-mark values decoded from state
|
|
m := testModuleInline(t, map[string]string{
|
|
"main.tf": `
|
|
module "mod" {
|
|
source = "./mod"
|
|
}
|
|
|
|
resource "test_object" "a" {
|
|
test_string = module.mod.out
|
|
}
|
|
`,
|
|
|
|
"mod/main.tf": `
|
|
variable "in" {
|
|
sensitive = true
|
|
default = "foo"
|
|
}
|
|
output "out" {
|
|
value = var.in
|
|
}
|
|
`,
|
|
})
|
|
|
|
p := simpleMockProvider()
|
|
|
|
ctx := testContext2(t, &ContextOpts{
|
|
Config: m,
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
|
},
|
|
})
|
|
|
|
_, diags := ctx.Plan()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.ErrWithWarnings())
|
|
}
|
|
|
|
state, diags := ctx.Apply()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.ErrWithWarnings())
|
|
}
|
|
|
|
obj := state.ResourceInstance(mustResourceInstanceAddr("test_object.a"))
|
|
if len(obj.Current.AttrSensitivePaths) != 1 {
|
|
t.Fatalf("Expected 1 sensitive mark for test_object.a, got %#v\n", obj.Current.AttrSensitivePaths)
|
|
}
|
|
|
|
plan, diags := ctx.Plan()
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.ErrWithWarnings())
|
|
}
|
|
|
|
// make sure the same marks are compared in the next plan as well
|
|
for _, c := range plan.Changes.Resources {
|
|
if c.Action != plans.NoOp {
|
|
t.Errorf("Unexpcetd %s change for %s", c.Action, c.Addr)
|
|
}
|
|
}
|
|
}
|