mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
This is part of a general effort to move all of Terraform's non-library package surface under internal in order to reinforce that these are for internal use within Terraform only. If you were previously importing packages under this prefix into an external codebase, you could pin to an earlier release tag as an interim solution until you've make a plan to achieve the same functionality some other way.
510 lines
14 KiB
Go
510 lines
14 KiB
Go
package terraform
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
"github.com/hashicorp/terraform/internal/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)
|
|
}
|
|
}
|
|
}
|