opentofu/internal/terraform/context_apply_test.go
Martin Atkins bee7403f3e command/workspace_delete: Allow deleting a workspace with empty husks
Previously we would reject attempts to delete a workspace if its state
contained any resources at all, even if none of the resources had any
resource instance objects associated with it.

Nowadays there isn't any situation where the normal Terraform workflow
will leave behind resource husks, and so this isn't as problematic as it
might've been in the v0.12 era, but nonetheless what we actually care
about for this check is whether there might be any remote objects that
this state is tracking, and for that it's more precise to look for
non-nil resource instance objects, rather than whole resources.

This also includes some adjustments to our error messaging to give more
information about the problem and to use terminology more consistent with
how we currently talk about this situation in our documentation and
elsewhere in the UI.

We were also using the old State.HasResources method as part of some of
our tests. I considered preserving it to avoid changing the behavior of
those tests, but the new check seemed close enough to the intent of those
tests that it wasn't worth maintaining this method that wouldn't be used
in any main code anymore. I've therefore updated those tests to use
the new HasResourceInstanceObjects method instead.
2021-10-13 13:54:11 -07:00

12427 lines
345 KiB
Go

package terraform
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"reflect"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/go-test/deep"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/configs/hcl2shim"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/provisioners"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
func TestContext2Apply_basic(t *testing.T) {
m := testModule(t, "apply-good")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) < 2 {
t.Fatalf("bad: %#v", mod.Resources)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_unstable(t *testing.T) {
// This tests behavior when the configuration contains an unstable value,
// such as the result of uuid() or timestamp(), where each call produces
// a different result.
//
// This is an important case to test because we need to ensure that
// we don't re-call the function during the apply phase: the value should
// be fixed during plan
m := testModule(t, "apply-unstable")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("unexpected error during Plan: %s", diags.Err())
}
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"].Block
rds := plan.Changes.ResourceInstance(addr)
rd, err := rds.Decode(schema.ImpliedType())
if err != nil {
t.Fatal(err)
}
if rd.After.GetAttr("random").IsKnown() {
t.Fatalf("Attribute 'random' has known value %#v; should be unknown in plan", rd.After.GetAttr("random"))
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("unexpected error during Apply: %s", diags.Err())
}
mod := state.Module(addr.Module)
rss := state.ResourceInstance(addr)
if len(mod.Resources) != 1 {
t.Fatalf("wrong number of resources %d; want 1", len(mod.Resources))
}
rs, err := rss.Current.Decode(schema.ImpliedType())
if err != nil {
t.Fatalf("decode error: %v", err)
}
got := rs.Value.GetAttr("random")
if !got.IsKnown() {
t.Fatalf("random is still unknown after apply")
}
if got, want := len(got.AsString()), 36; got != want {
t.Fatalf("random string has wrong length %d; want %d", got, want)
}
}
func TestContext2Apply_escape(t *testing.T) {
m := testModule(t, "apply-escape")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.bar:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = "bar"
type = aws_instance
`)
}
func TestContext2Apply_resourceCountOneList(t *testing.T) {
m := testModule(t, "apply-resource-count-one-list")
p := testProvider("null")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoDiagnostics(t, diags)
got := strings.TrimSpace(state.String())
want := strings.TrimSpace(`null_resource.foo.0:
ID = foo
provider = provider["registry.terraform.io/hashicorp/null"]
Outputs:
test = [foo]`)
if got != want {
t.Fatalf("got:\n%s\n\nwant:\n%s\n", got, want)
}
}
func TestContext2Apply_resourceCountZeroList(t *testing.T) {
m := testModule(t, "apply-resource-count-zero-list")
p := testProvider("null")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
got := strings.TrimSpace(state.String())
want := strings.TrimSpace(`<no state>
Outputs:
test = []`)
if got != want {
t.Fatalf("wrong state\n\ngot:\n%s\n\nwant:\n%s\n", got, want)
}
}
func TestContext2Apply_resourceDependsOnModule(t *testing.T) {
m := testModule(t, "apply-resource-depends-on-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// verify the apply happens in the correct order
var mu sync.Mutex
var order []string
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
ami := req.PlannedState.GetAttr("ami").AsString()
switch ami {
case "child":
// make the child slower than the parent
time.Sleep(50 * time.Millisecond)
mu.Lock()
order = append(order, "child")
mu.Unlock()
case "parent":
mu.Lock()
order = append(order, "parent")
mu.Unlock()
}
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if !reflect.DeepEqual(order, []string{"child", "parent"}) {
t.Fatal("resources applied out of order")
}
checkStateString(t, state, testTerraformApplyResourceDependsOnModuleStr)
}
// Test that without a config, the Dependencies in the state are enough
// to maintain proper ordering.
func TestContext2Apply_resourceDependsOnModuleStateOnly(t *testing.T) {
m := testModule(t, "apply-resource-depends-on-module-empty")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"parent"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.child.aws_instance.child")},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.child").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"child"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
{
// verify the apply happens in the correct order
var mu sync.Mutex
var order []string
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
id := req.PriorState.GetAttr("id")
if id.IsKnown() && id.AsString() == "parent" {
// make the dep slower than the parent
time.Sleep(50 * time.Millisecond)
mu.Lock()
order = append(order, "child")
mu.Unlock()
} else {
mu.Lock()
order = append(order, "parent")
mu.Unlock()
}
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
if !reflect.DeepEqual(order, []string{"child", "parent"}) {
t.Fatal("resources applied out of order")
}
checkStateString(t, state, "<no state>")
}
}
func TestContext2Apply_resourceDependsOnModuleDestroy(t *testing.T) {
m := testModule(t, "apply-resource-depends-on-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
var globalState *states.State
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
globalState = state
}
{
// Wait for the dependency, sleep, and verify the graph never
// called a child.
var called int32
var checked bool
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
ami := req.PriorState.GetAttr("ami").AsString()
if ami == "parent" {
checked = true
// Sleep to allow parallel execution
time.Sleep(50 * time.Millisecond)
// Verify that called is 0 (dep not called)
if atomic.LoadInt32(&called) != 0 {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("module child should not be called"))
return resp
}
}
atomic.AddInt32(&called, 1)
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, globalState, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if !checked {
t.Fatal("should check")
}
checkStateString(t, state, `<no state>`)
}
}
func TestContext2Apply_resourceDependsOnModuleGrandchild(t *testing.T) {
m := testModule(t, "apply-resource-depends-on-module-deep")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
{
// Wait for the dependency, sleep, and verify the graph never
// called a child.
var called int32
var checked bool
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
planned := req.PlannedState.AsValueMap()
if ami, ok := planned["ami"]; ok && ami.AsString() == "grandchild" {
checked = true
// Sleep to allow parallel execution
time.Sleep(50 * time.Millisecond)
// Verify that called is 0 (dep not called)
if atomic.LoadInt32(&called) != 0 {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("aws_instance.a should not be called"))
return resp
}
}
atomic.AddInt32(&called, 1)
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if !checked {
t.Fatal("should check")
}
checkStateString(t, state, testTerraformApplyResourceDependsOnModuleDeepStr)
}
}
func TestContext2Apply_resourceDependsOnModuleInModule(t *testing.T) {
m := testModule(t, "apply-resource-depends-on-module-in-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
{
// Wait for the dependency, sleep, and verify the graph never
// called a child.
var called int32
var checked bool
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
planned := req.PlannedState.AsValueMap()
if ami, ok := planned["ami"]; ok && ami.AsString() == "grandchild" {
checked = true
// Sleep to allow parallel execution
time.Sleep(50 * time.Millisecond)
// Verify that called is 0 (dep not called)
if atomic.LoadInt32(&called) != 0 {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("something else was applied before grandchild; grandchild should be first"))
return resp
}
}
atomic.AddInt32(&called, 1)
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if !checked {
t.Fatal("should check")
}
checkStateString(t, state, testTerraformApplyResourceDependsOnModuleInModuleStr)
}
}
func TestContext2Apply_mapVarBetweenModules(t *testing.T) {
m := testModule(t, "apply-map-var-through-module")
p := testProvider("null")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`<no state>
Outputs:
amis_from_module = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 }
module.test:
null_resource.noop:
ID = foo
provider = provider["registry.terraform.io/hashicorp/null"]
Outputs:
amis_out = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 }`)
if actual != expected {
t.Fatalf("expected: \n%s\n\ngot: \n%s\n", expected, actual)
}
}
func TestContext2Apply_refCount(t *testing.T) {
m := testModule(t, "apply-ref-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) < 2 {
t.Fatalf("bad: %#v", mod.Resources)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyRefCountStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_providerAlias(t *testing.T) {
m := testModule(t, "apply-provider-alias")
// Each provider instance must be completely independent to ensure that we
// are verifying the correct state of each.
p := func() (providers.Interface, error) {
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
return p, nil
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): p,
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) < 2 {
t.Fatalf("bad: %#v", mod.Resources)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProviderAliasStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
// Two providers that are configured should both be configured prior to apply
func TestContext2Apply_providerAliasConfigure(t *testing.T) {
m := testModule(t, "apply-provider-alias-configure")
// Each provider instance must be completely independent to ensure that we
// are verifying the correct state of each.
p := func() (providers.Interface, error) {
p := testProvider("another")
p.ApplyResourceChangeFn = testApplyFn
p.PlanResourceChangeFn = testDiffFn
return p, nil
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("another"): p,
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
// Configure to record calls AFTER Plan above
var configCount int32
p = func() (providers.Interface, error) {
p := testProvider("another")
p.ApplyResourceChangeFn = testApplyFn
p.PlanResourceChangeFn = testDiffFn
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
atomic.AddInt32(&configCount, 1)
foo := req.Config.GetAttr("foo").AsString()
if foo != "bar" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("foo: %#v", foo))
}
return
}
return p, nil
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("another"): p,
},
})
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if configCount != 2 {
t.Fatalf("provider config expected 2 calls, got: %d", configCount)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProviderAliasConfigStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
// GH-2870
func TestContext2Apply_providerWarning(t *testing.T) {
m := testModule(t, "apply-provider-warning")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
p.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) {
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.SimpleWarning("just a warning"))
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`)
if actual != expected {
t.Fatalf("got: \n%s\n\nexpected:\n%s", actual, expected)
}
if !p.ConfigureProviderCalled {
t.Fatalf("provider Configure() was never called!")
}
}
func TestContext2Apply_emptyModule(t *testing.T) {
// A module with only outputs (no resources)
m := testModule(t, "apply-empty-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
actual = strings.Replace(actual, " ", "", -1)
expected := strings.TrimSpace(testTerraformApplyEmptyModuleStr)
if actual != expected {
t.Fatalf("bad: \n%s\nexpect:\n%s", actual, expected)
}
}
func TestContext2Apply_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-good-create-before")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar", "require_new": "abc"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if got, want := len(mod.Resources), 1; got != want {
t.Logf("state:\n%s", state)
t.Fatalf("wrong number of resources %d; want %d", got, want)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyCreateBeforeStr)
if actual != expected {
t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual)
}
}
func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) {
m := testModule(t, "apply-good-create-before-update")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// signal that resource foo has started applying
fooChan := make(chan struct{})
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
id := req.PriorState.GetAttr("id").AsString()
switch id {
case "bar":
select {
case <-fooChan:
resp.Diagnostics = resp.Diagnostics.Append(errors.New("bar must be updated before foo is destroyed"))
return resp
case <-time.After(100 * time.Millisecond):
// wait a moment to ensure that foo is not going to be destroyed first
}
case "foo":
close(fooChan)
}
return testApplyFn(req)
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
fooAddr := mustResourceInstanceAddr("aws_instance.foo")
root.SetResourceInstanceCurrent(
fooAddr.Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`),
CreateBeforeDestroy: true,
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","foo":"bar"}`),
CreateBeforeDestroy: true,
Dependencies: []addrs.ConfigResource{fooAddr.ContainingResource().Config()},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) != 1 {
t.Fatalf("bad: %s", state)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyCreateBeforeUpdateStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
// This tests that when a CBD resource depends on a non-CBD resource,
// we can still properly apply changes that require new for both.
func TestContext2Apply_createBeforeDestroy_dependsNonCBD(t *testing.T) {
m := testModule(t, "apply-cbd-depends-non-cbd")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar", "require_new": "abc"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo", "require_new": "abc"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.bar:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
require_new = yes
type = aws_instance
value = foo
Dependencies:
aws_instance.foo
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
require_new = yes
type = aws_instance
`)
}
func TestContext2Apply_createBeforeDestroy_hook(t *testing.T) {
h := new(MockHook)
m := testModule(t, "apply-good-create-before")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar", "require_new": "abc"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
var actual []cty.Value
var actualLock sync.Mutex
h.PostApplyFn = func(addr addrs.AbsResourceInstance, gen states.Generation, sv cty.Value, e error) (HookAction, error) {
actualLock.Lock()
defer actualLock.Unlock()
actual = append(actual, sv)
return HookActionContinue, nil
}
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
expected := []cty.Value{
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"require_new": cty.StringVal("xyz"),
"type": cty.StringVal("aws_instance"),
}),
cty.NullVal(cty.DynamicPseudoType),
}
cmpOpt := cmp.Transformer("ctyshim", hcl2shim.ConfigValueFromHCL2)
if !cmp.Equal(actual, expected, cmpOpt) {
t.Fatalf("wrong state snapshot sequence\n%s", cmp.Diff(expected, actual, cmpOpt))
}
}
// Test that we can perform an apply with CBD in a count with deposed instances.
func TestContext2Apply_createBeforeDestroy_deposedCount(t *testing.T) {
m := testModule(t, "apply-cbd-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceDeposed(
mustResourceInstanceAddr("aws_instance.bar[0]").Resource,
states.NewDeposedKey(),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar[1]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceDeposed(
mustResourceInstanceAddr("aws_instance.bar[1]").Resource,
states.NewDeposedKey(),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.bar.0:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = bar
type = aws_instance
aws_instance.bar.1:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = bar
type = aws_instance
`)
}
// Test that when we have a deposed instance but a good primary, we still
// destroy the deposed instance.
func TestContext2Apply_createBeforeDestroy_deposedOnly(t *testing.T) {
m := testModule(t, "apply-cbd-deposed-only")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
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,
states.NewDeposedKey(),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.bar:
ID = bar
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`)
}
func TestContext2Apply_destroyComputed(t *testing.T) {
m := testModule(t, "apply-destroy-computed")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo", "output": "value"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("plan failed")
} else {
t.Logf("plan:\n\n%s", legacyDiffComparisonString(plan.Changes))
}
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("apply failed")
}
}
// Test that the destroy operation uses depends_on as a source of ordering.
func TestContext2Apply_destroyDependsOn(t *testing.T) {
// It is possible for this to be racy, so we loop a number of times
// just to check.
for i := 0; i < 10; i++ {
testContext2Apply_destroyDependsOn(t)
}
}
func testContext2Apply_destroyDependsOn(t *testing.T) {
m := testModule(t, "apply-destroy-depends-on")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
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.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("aws_instance.bar")},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
// Record the order we see Apply
var actual []string
var actualLock sync.Mutex
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
actualLock.Lock()
defer actualLock.Unlock()
id := req.PriorState.GetAttr("id").AsString()
actual = append(actual, id)
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Parallelism: 1, // To check ordering
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
expected := []string{"foo", "bar"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("wrong order\ngot: %#v\nwant: %#v", actual, expected)
}
}
// Test that destroy ordering is correct with dependencies only
// in the state.
func TestContext2Apply_destroyDependsOnStateOnly(t *testing.T) {
newState := states.NewState()
root := newState.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
Dependencies: []addrs.ConfigResource{},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
},
Module: root.Addr.Module(),
},
},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
// It is possible for this to be racy, so we loop a number of times
// just to check.
for i := 0; i < 10; i++ {
t.Run("new", func(t *testing.T) {
testContext2Apply_destroyDependsOnStateOnly(t, newState)
})
}
}
func testContext2Apply_destroyDependsOnStateOnly(t *testing.T, state *states.State) {
state = state.DeepCopy()
m := testModule(t, "empty")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// Record the order we see Apply
var actual []string
var actualLock sync.Mutex
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
actualLock.Lock()
defer actualLock.Unlock()
id := req.PriorState.GetAttr("id").AsString()
actual = append(actual, id)
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Parallelism: 1, // To check ordering
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
expected := []string{"bar", "foo"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("wrong order\ngot: %#v\nwant: %#v", actual, expected)
}
}
// Test that destroy ordering is correct with dependencies only
// in the state within a module (GH-11749)
func TestContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T) {
newState := states.NewState()
child := newState.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
Dependencies: []addrs.ConfigResource{},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
child.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
},
Module: child.Addr.Module(),
},
},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
// It is possible for this to be racy, so we loop a number of times
// just to check.
for i := 0; i < 10; i++ {
t.Run("new", func(t *testing.T) {
testContext2Apply_destroyDependsOnStateOnlyModule(t, newState)
})
}
}
func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T, state *states.State) {
state = state.DeepCopy()
m := testModule(t, "empty")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// Record the order we see Apply
var actual []string
var actualLock sync.Mutex
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
actualLock.Lock()
defer actualLock.Unlock()
id := req.PriorState.GetAttr("id").AsString()
actual = append(actual, id)
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Parallelism: 1, // To check ordering
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
expected := []string{"bar", "foo"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("wrong order\ngot: %#v\nwant: %#v", actual, expected)
}
}
func TestContext2Apply_dataBasic(t *testing.T) {
m := testModule(t, "apply-data-basic")
p := testProvider("null")
p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("yo"),
"foo": cty.NullVal(cty.String),
}),
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyDataBasicStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_destroyData(t *testing.T) {
m := testModule(t, "apply-destroy-data-resource")
p := testProvider("null")
p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
return providers.ReadDataSourceResponse{
State: req.Config,
}
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("data.null_data_source.testing").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"-"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/null"]`),
)
hook := &testHook{}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
Hooks: []Hook{hook},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
newState, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if got := len(newState.Modules); got != 1 {
t.Fatalf("state has %d modules after destroy; want 1", got)
}
if got := len(newState.RootModule().Resources); got != 0 {
t.Fatalf("state has %d resources after destroy; want 0", got)
}
wantHookCalls := []*testHookCall{
{"PreDiff", "data.null_data_source.testing"},
{"PostDiff", "data.null_data_source.testing"},
{"PreDiff", "data.null_data_source.testing"},
{"PostDiff", "data.null_data_source.testing"},
{"PostStateUpdate", ""},
}
if !reflect.DeepEqual(hook.Calls, wantHookCalls) {
t.Errorf("wrong hook calls\ngot: %swant: %s", spew.Sdump(hook.Calls), spew.Sdump(wantHookCalls))
}
}
// https://github.com/hashicorp/terraform/pull/5096
func TestContext2Apply_destroySkipsCBD(t *testing.T) {
// Config contains CBD resource depending on non-CBD resource, which triggers
// a cycle if they are both replaced, but should _not_ trigger a cycle when
// just doing a `terraform destroy`.
m := testModule(t, "apply-destroy-cbd")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_destroyModuleVarProviderConfig(t *testing.T) {
m := testModule(t, "apply-destroy-mod-var-provider-config")
p := func() (providers.Interface, error) {
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
return p, nil
}
state := states.NewState()
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): p,
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
}
func TestContext2Apply_destroyCrossProviders(t *testing.T) {
m := testModule(t, "apply-destroy-cross-providers")
p_aws := testProvider("aws")
p_aws.ApplyResourceChangeFn = testApplyFn
p_aws.PlanResourceChangeFn = testDiffFn
p_aws.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
},
},
"aws_vpc": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"value": {
Type: cty.String,
Optional: true,
},
},
},
},
})
providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p_aws),
}
ctx, m, state := getContextForApply_destroyCrossProviders(t, m, providers)
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("apply failed")
}
}
func getContextForApply_destroyCrossProviders(t *testing.T, m *configs.Config, providerFactories map[addrs.Provider]providers.Factory) (*Context, *configs.Config, *states.State) {
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.shared").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"test"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_vpc.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id": "vpc-aaabbb12", "value":"test"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: providerFactories,
})
return ctx, m, state
}
func TestContext2Apply_minimal(t *testing.T) {
m := testModule(t, "apply-minimal")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyMinimalStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_cancel(t *testing.T) {
stopped := false
m := testModule(t, "apply-cancel")
p := testProvider("aws")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
if !stopped {
stopped = true
go ctx.Stop()
for {
if ctx.sh.Stopped() {
break
}
time.Sleep(10 * time.Millisecond)
}
}
return testApplyFn(req)
}
p.PlanResourceChangeFn = testDiffFn
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
// Start the Apply in a goroutine
var applyDiags tfdiags.Diagnostics
stateCh := make(chan *states.State)
go func() {
state, diags := ctx.Apply(plan, m)
applyDiags = diags
stateCh <- state
}()
state := <-stateCh
// only expecting an early exit error
if !applyDiags.HasErrors() {
t.Fatal("expected early exit error")
}
for _, d := range applyDiags {
desc := d.Description()
if desc.Summary != "execution halted" {
t.Fatalf("unexpected error: %v", applyDiags.Err())
}
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyCancelStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
if !p.StopCalled {
t.Fatal("stop should be called")
}
}
func TestContext2Apply_cancelBlock(t *testing.T) {
m := testModule(t, "apply-cancel-block")
p := testProvider("aws")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
applyCh := make(chan struct{})
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
close(applyCh)
for !ctx.sh.Stopped() {
// Wait for stop to be called. We call Gosched here so that
// the other goroutines can always be scheduled to set Stopped.
runtime.Gosched()
}
// Sleep
time.Sleep(100 * time.Millisecond)
return testApplyFn(req)
}
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
// Start the Apply in a goroutine
var applyDiags tfdiags.Diagnostics
stateCh := make(chan *states.State)
go func() {
state, diags := ctx.Apply(plan, m)
applyDiags = diags
stateCh <- state
}()
stopDone := make(chan struct{})
go func() {
defer close(stopDone)
<-applyCh
ctx.Stop()
}()
// Make sure that stop blocks
select {
case <-stopDone:
t.Fatal("stop should block")
case <-time.After(10 * time.Millisecond):
}
// Wait for stop
select {
case <-stopDone:
case <-time.After(500 * time.Millisecond):
t.Fatal("stop should be done")
}
// Wait for apply to complete
state := <-stateCh
// only expecting an early exit error
if !applyDiags.HasErrors() {
t.Fatal("expected early exit error")
}
for _, d := range applyDiags {
desc := d.Description()
if desc.Summary != "execution halted" {
t.Fatalf("unexpected error: %v", applyDiags.Err())
}
}
checkStateString(t, state, `
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
num = 2
type = aws_instance
`)
}
func TestContext2Apply_cancelProvisioner(t *testing.T) {
m := testModule(t, "apply-cancel-provisioner")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr := testProvisioner()
pr.GetSchemaResponse = provisioners.GetSchemaResponse{
Provisioner: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
prStopped := make(chan struct{})
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
// Start the stop process
go ctx.Stop()
<-prStopped
return
}
pr.StopFn = func() error {
close(prStopped)
return nil
}
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
// Start the Apply in a goroutine
var applyDiags tfdiags.Diagnostics
stateCh := make(chan *states.State)
go func() {
state, diags := ctx.Apply(plan, m)
applyDiags = diags
stateCh <- state
}()
// Wait for completion
state := <-stateCh
// we are expecting only an early exit error
if !applyDiags.HasErrors() {
t.Fatal("expected early exit error")
}
for _, d := range applyDiags {
desc := d.Description()
if desc.Summary != "execution halted" {
t.Fatalf("unexpected error: %v", applyDiags.Err())
}
}
checkStateString(t, state, `
aws_instance.foo: (tainted)
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
num = 2
type = aws_instance
`)
if !pr.StopCalled {
t.Fatal("stop should be called")
}
}
func TestContext2Apply_compute(t *testing.T) {
m := testModule(t, "apply-compute")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"num": {
Type: cty.Number,
Optional: true,
},
"compute": {
Type: cty.String,
Optional: true,
},
"compute_value": {
Type: cty.String,
Optional: true,
},
"foo": {
Type: cty.String,
Optional: true,
},
"id": {
Type: cty.String,
Computed: true,
},
"type": {
Type: cty.String,
Computed: true,
},
"value": { // Populated from compute_value because compute = "value" in the config fixture
Type: cty.String,
Computed: true,
},
},
},
},
})
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
SetVariables: InputValues{
"value": &InputValue{
Value: cty.NumberIntVal(1),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyComputeStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_countDecrease(t *testing.T) {
m := testModule(t, "apply-count-dec")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","foo": "foo","type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[1]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","foo": "foo","type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[2]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar", "foo": "foo", "type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testTerraformApplyCountDecStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_countDecreaseToOneX(t *testing.T) {
m := testModule(t, "apply-count-dec-one")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar", "foo": "foo", "type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[1]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[2]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testTerraformApplyCountDecToOneStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
// https://github.com/PeoplePerHour/terraform/pull/11
//
// This tests a rare but possible situation where we have both a no-key and
// a zero-key instance of the same resource in the configuration when we
// disable count.
//
// The main way to get here is for a provider to fail to destroy the zero-key
// instance but succeed in creating the no-key instance, since those two
// can typically happen concurrently. There are various other ways to get here
// that might be considered user error, such as using "terraform state mv"
// to create a strange combination of different key types on the same resource.
//
// This test indirectly exercises an intentional interaction between
// refactoring.ImpliedMoveStatements and refactoring.ApplyMoves: we'll first
// generate an implied move statement from aws_instance.foo[0] to
// aws_instance.foo, but then refactoring.ApplyMoves should notice that and
// ignore the statement, in the same way as it would if an explicit move
// statement specified the same situation.
func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) {
m := testModule(t, "apply-count-dec-one")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar", "foo": "foo", "type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"baz", "type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
{
got := strings.TrimSpace(legacyPlanComparisonString(state, plan.Changes))
want := strings.TrimSpace(testTerraformApplyCountDecToOneCorruptedPlanStr)
if got != want {
t.Fatalf("wrong plan result\ngot:\n%s\nwant:\n%s", got, want)
}
}
{
change := plan.Changes.ResourceInstance(mustResourceInstanceAddr("aws_instance.foo[0]"))
if change == nil {
t.Fatalf("no planned change for instance zero")
}
if got, want := change.Action, plans.Delete; got != want {
t.Errorf("wrong action for instance zero %s; want %s", got, want)
}
if got, want := change.ActionReason, plans.ResourceInstanceDeleteBecauseWrongRepetition; got != want {
t.Errorf("wrong action reason for instance zero %s; want %s", got, want)
}
}
{
change := plan.Changes.ResourceInstance(mustResourceInstanceAddr("aws_instance.foo"))
if change == nil {
t.Fatalf("no planned change for no-key instance")
}
if got, want := change.Action, plans.NoOp; got != want {
t.Errorf("wrong action for no-key instance %s; want %s", got, want)
}
if got, want := change.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
t.Errorf("wrong action reason for no-key instance %s; want %s", got, want)
}
}
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testTerraformApplyCountDecToOneCorruptedStr)
if actual != expected {
t.Fatalf("wrong final state\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_countTainted(t *testing.T) {
m := testModule(t, "apply-count-tainted")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"bar", "type": "aws_instance", "foo": "foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
{
got := strings.TrimSpace(legacyDiffComparisonString(plan.Changes))
want := strings.TrimSpace(`
DESTROY/CREATE: aws_instance.foo[0]
foo: "foo" => "foo"
id: "bar" => "<computed>"
type: "aws_instance" => "<computed>"
CREATE: aws_instance.foo[1]
foo: "" => "foo"
id: "" => "<computed>"
type: "" => "<computed>"
`)
if got != want {
t.Fatalf("wrong plan\n\ngot:\n%s\n\nwant:\n%s", got, want)
}
}
s, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
got := strings.TrimSpace(s.String())
want := strings.TrimSpace(`
aws_instance.foo.0:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = foo
type = aws_instance
aws_instance.foo.1:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = foo
type = aws_instance
`)
if got != want {
t.Fatalf("wrong final state\n\ngot:\n%s\n\nwant:\n%s", got, want)
}
}
func TestContext2Apply_countVariable(t *testing.T) {
m := testModule(t, "apply-count-variable")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyCountVariableStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_countVariableRef(t *testing.T) {
m := testModule(t, "apply-count-variable-ref")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyCountVariableRefStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_provisionerInterpCount(t *testing.T) {
// This test ensures that a provisioner can interpolate a resource count
// even though the provisioner expression is evaluated during the plan
// walk. https://github.com/hashicorp/terraform/issues/16840
m, snap := testModuleWithSnapshot(t, "apply-provisioner-interp-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
pr := testProvisioner()
Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
}
provisioners := map[string]provisioners.Factory{
"local-exec": testProvisionerFuncFixed(pr),
}
ctx := testContext2(t, &ContextOpts{
Providers: Providers,
Provisioners: provisioners,
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
// We'll marshal and unmarshal the plan here, to ensure that we have
// a clean new context as would be created if we separately ran
// terraform plan -out=tfplan && terraform apply tfplan
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatal(err)
}
ctxOpts.Providers = Providers
ctxOpts.Provisioners = provisioners
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("failed to create context for plan: %s", diags.Err())
}
// Applying the plan should now succeed
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply failed unexpectedly: %s", diags.Err())
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner was not called")
}
}
func TestContext2Apply_foreachVariable(t *testing.T) {
m := testModule(t, "plan-for-each-unknown-value")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"foo": &InputValue{
Value: cty.StringVal("hello"),
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyForEachVariableStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_moduleBasic(t *testing.T) {
m := testModule(t, "apply-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyModuleStr)
if actual != expected {
t.Fatalf("bad, expected:\n%s\n\nactual:\n%s", expected, actual)
}
}
func TestContext2Apply_moduleDestroyOrder(t *testing.T) {
m := testModule(t, "apply-module-destroy-order")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// Create a custom apply function to track the order they were destroyed
var order []string
var orderLock sync.Mutex
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
id := req.PriorState.GetAttr("id").AsString()
if id == "b" {
// Pause briefly to make any race conditions more visible, since
// missing edges here can cause undeterministic ordering.
time.Sleep(100 * time.Millisecond)
}
orderLock.Lock()
defer orderLock.Unlock()
order = append(order, id)
resp.NewState = req.PlannedState
return resp
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Required: true},
"blah": {Type: cty.String, Optional: true},
"value": {Type: cty.String, Optional: true},
},
},
},
})
state := states.NewState()
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.b").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"b"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.child.aws_instance.a")},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
expected := []string{"b", "a"}
if !reflect.DeepEqual(order, expected) {
t.Errorf("wrong order\ngot: %#v\nwant: %#v", order, expected)
}
{
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyModuleDestroyOrderStr)
if actual != expected {
t.Errorf("wrong final state\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
}
func TestContext2Apply_moduleInheritAlias(t *testing.T) {
m := testModule(t, "apply-module-provider-inherit-alias")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
val := req.Config.GetAttr("value")
if val.IsNull() {
return
}
root := req.Config.GetAttr("root")
if !root.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("child should not get root"))
}
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
<no state>
module.child:
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"].eu
type = aws_instance
`)
}
func TestContext2Apply_orphanResource(t *testing.T) {
// This is a two-step test:
// 1. Apply a configuration with resources that have count set.
// This should place the empty resource object in the state to record
// that each exists, and record any instances.
// 2. Apply an empty configuration against the same state, which should
// then clean up both the instances and the containing resource objects.
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_thing": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"foo": {Type: cty.String, Optional: true},
},
},
},
})
// Step 1: create the resources and instances
m := testModule(t, "apply-orphan-resource")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
// At this point both resources should be recorded in the state, along
// with the single instance associated with test_thing.one.
want := states.BuildState(func(s *states.SyncState) {
providerAddr := addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
}
oneAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "one",
}.Absolute(addrs.RootModuleInstance)
s.SetResourceProvider(oneAddr, providerAddr)
s.SetResourceInstanceCurrent(oneAddr.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
}, providerAddr)
})
if state.String() != want.String() {
t.Fatalf("wrong state after step 1\n%s", cmp.Diff(want, state))
}
// Step 2: update with an empty config, to destroy everything
m = testModule(t, "empty")
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
{
addr := mustResourceInstanceAddr("test_thing.one[0]")
change := plan.Changes.ResourceInstance(addr)
if change == nil {
t.Fatalf("no planned change for %s", addr)
}
if got, want := change.Action, plans.Delete; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
if got, want := change.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
}
state, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
// The state should now be _totally_ empty, with just an empty root module
// (since that always exists) and no resources at all.
want = states.NewState()
if !cmp.Equal(state, want) {
t.Fatalf("wrong state after step 2\ngot: %swant: %s", spew.Sdump(state), spew.Sdump(want))
}
}
func TestContext2Apply_moduleOrphanInheritAlias(t *testing.T) {
m := testModule(t, "apply-module-provider-inherit-alias-orphan")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
val := req.Config.GetAttr("value")
if val.IsNull() {
return
}
root := req.Config.GetAttr("root")
if !root.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("child should not get root"))
}
return
}
// Create a state with an orphan module
state := states.NewState()
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
{
addr := mustResourceInstanceAddr("module.child.aws_instance.bar")
change := plan.Changes.ResourceInstance(addr)
if change == nil {
t.Fatalf("no planned change for %s", addr)
}
if got, want := change.Action, plans.Delete; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
// This should ideally be ResourceInstanceDeleteBecauseNoModule, but
// the codepath deciding this doesn't currently have enough information
// to differentiate, and so this is a compromise.
if got, want := change.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if !p.ConfigureProviderCalled {
t.Fatal("must call configure")
}
checkStateString(t, state, "<no state>")
}
func TestContext2Apply_moduleOrphanProvider(t *testing.T) {
m := testModule(t, "apply-module-orphan-provider-inherit")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
val := req.Config.GetAttr("value")
if val.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found"))
}
return
}
// Create a state with an orphan module
state := states.NewState()
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_moduleOrphanGrandchildProvider(t *testing.T) {
m := testModule(t, "apply-module-orphan-provider-inherit")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
val := req.Config.GetAttr("value")
if val.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found"))
}
return
}
// Create a state with an orphan module that is nested (grandchild)
state := states.NewState()
child := state.EnsureModule(addrs.RootModuleInstance.Child("parent", addrs.NoKey).Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_moduleGrandchildProvider(t *testing.T) {
m := testModule(t, "apply-module-grandchild-provider-inherit")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
var callLock sync.Mutex
called := false
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
val := req.Config.GetAttr("value")
if val.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found"))
}
callLock.Lock()
called = true
callLock.Unlock()
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
callLock.Lock()
defer callLock.Unlock()
if called != true {
t.Fatalf("err: configure never called")
}
}
// This tests an issue where all the providers in a module but not
// in the root weren't being added to the root properly. In this test
// case: aws is explicitly added to root, but "test" should be added to.
// With the bug, it wasn't.
func TestContext2Apply_moduleOnlyProvider(t *testing.T) {
m := testModule(t, "apply-module-only-provider")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pTest := testProvider("test")
pTest.ApplyResourceChangeFn = testApplyFn
pTest.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("test"): testProviderFuncFixed(pTest),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyModuleOnlyProviderStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_moduleProviderAlias(t *testing.T) {
m := testModule(t, "apply-module-provider-alias")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyModuleProviderAliasStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_moduleProviderAliasTargets(t *testing.T) {
m := testModule(t, "apply-module-provider-alias")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.ConfigResource{
Module: addrs.RootModule,
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "nonexistent",
Name: "thing",
},
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`
<no state>
`)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_moduleProviderCloseNested(t *testing.T) {
m := testModule(t, "apply-module-provider-close-nested")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
// Tests that variables used as module vars that reference data that
// already exists in the state and requires no diff works properly. This
// fixes an issue faced where module variables were pruned because they were
// accessing "non-existent" resources (they existed, just not in the graph
// cause they weren't in the diff).
func TestContext2Apply_moduleVarRefExisting(t *testing.T) {
m := testModule(t, "apply-ref-existing")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyModuleVarRefExistingStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_moduleVarResourceCount(t *testing.T) {
m := testModule(t, "apply-module-var-resource-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.DestroyMode,
SetVariables: InputValues{
"num": &InputValue{
Value: cty.NumberIntVal(2),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"num": &InputValue{
Value: cty.NumberIntVal(5),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
// GH-819
func TestContext2Apply_moduleBool(t *testing.T) {
m := testModule(t, "apply-module-bool")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyModuleBoolStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
// Tests that a module can be targeted and everything is properly created.
// This adds to the plan test to also just verify that apply works.
func TestContext2Apply_moduleTarget(t *testing.T) {
m := testModule(t, "plan-targeted-cross-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Child("B", addrs.NoKey),
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
<no state>
module.A:
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = bar
type = aws_instance
Outputs:
value = foo
module.B:
aws_instance.bar:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = foo
type = aws_instance
Dependencies:
module.A.aws_instance.foo
`)
}
func TestContext2Apply_multiProvider(t *testing.T) {
m := testModule(t, "apply-multi-provider")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pDO := testProvider("do")
pDO.ApplyResourceChangeFn = testApplyFn
pDO.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("do"): testProviderFuncFixed(pDO),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) < 2 {
t.Fatalf("bad: %#v", mod.Resources)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyMultiProviderStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_multiProviderDestroy(t *testing.T) {
m := testModule(t, "apply-multi-provider-destroy")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"addr": {Type: cty.String, Optional: true},
},
},
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"foo": {Type: cty.String, Optional: true},
},
},
},
})
p2 := testProvider("vault")
p2.ApplyResourceChangeFn = testApplyFn
p2.PlanResourceChangeFn = testDiffFn
p2.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"vault_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
})
var state *states.State
// First, create the instances
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("vault"): testProviderFuncFixed(p2),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
state = s
}
// Destroy them
{
// Verify that aws_instance.bar is destroyed first
var checked bool
var called int32
var lock sync.Mutex
applyFn := func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
lock.Lock()
defer lock.Unlock()
if req.TypeName == "aws_instance" {
checked = true
// Sleep to allow parallel execution
time.Sleep(50 * time.Millisecond)
// Verify that called is 0 (dep not called)
if atomic.LoadInt32(&called) != 0 {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("nothing else should be called"))
return resp
}
}
atomic.AddInt32(&called, 1)
return testApplyFn(req)
}
// Set the apply functions
p.ApplyResourceChangeFn = applyFn
p2.ApplyResourceChangeFn = applyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("vault"): testProviderFuncFixed(p2),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
if !checked {
t.Fatal("should be checked")
}
state = s
}
checkStateString(t, state, `<no state>`)
}
// This is like the multiProviderDestroy test except it tests that
// dependent resources within a child module that inherit provider
// configuration are still destroyed first.
func TestContext2Apply_multiProviderDestroyChild(t *testing.T) {
m := testModule(t, "apply-multi-provider-destroy-child")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
},
},
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"foo": {Type: cty.String, Optional: true},
},
},
},
})
p2 := testProvider("vault")
p2.ApplyResourceChangeFn = testApplyFn
p2.PlanResourceChangeFn = testDiffFn
p2.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
Provider: &configschema.Block{},
ResourceTypes: map[string]*configschema.Block{
"vault_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
})
var state *states.State
// First, create the instances
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("vault"): testProviderFuncFixed(p2),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
state = s
}
// Destroy them
{
// Verify that aws_instance.bar is destroyed first
var checked bool
var called int32
var lock sync.Mutex
applyFn := func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
lock.Lock()
defer lock.Unlock()
if req.TypeName == "aws_instance" {
checked = true
// Sleep to allow parallel execution
time.Sleep(50 * time.Millisecond)
// Verify that called is 0 (dep not called)
if atomic.LoadInt32(&called) != 0 {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("nothing else should be called"))
return resp
}
}
atomic.AddInt32(&called, 1)
return testApplyFn(req)
}
// Set the apply functions
p.ApplyResourceChangeFn = applyFn
p2.ApplyResourceChangeFn = applyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("vault"): testProviderFuncFixed(p2),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if !checked {
t.Fatal("should be checked")
}
state = s
}
checkStateString(t, state, `
<no state>
`)
}
func TestContext2Apply_multiVar(t *testing.T) {
m := testModule(t, "apply-multi-var")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// First, apply with a count of 3
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"num": &InputValue{
Value: cty.NumberIntVal(3),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := state.RootModule().OutputValues["output"]
expected := cty.StringVal("bar0,bar1,bar2")
if actual == nil || actual.Value != expected {
t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected)
}
t.Logf("Initial state: %s", state.String())
// Apply again, reduce the count to 1
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"num": &InputValue{
Value: cty.NumberIntVal(1),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
t.Logf("End state: %s", state.String())
actual := state.RootModule().OutputValues["output"]
if actual == nil {
t.Fatal("missing output")
}
expected := cty.StringVal("bar0")
if actual.Value != expected {
t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected)
}
}
}
// This is a holistic test of multi-var (aka "splat variable") handling
// across several different Terraform subsystems. This is here because
// historically there were quirky differences in handling across different
// parts of Terraform and so here we want to assert the expected behavior and
// ensure that it remains consistent in future.
func TestContext2Apply_multiVarComprehensive(t *testing.T) {
m := testModule(t, "apply-multi-var-comprehensive")
p := testProvider("test")
configs := map[string]cty.Value{}
var configsLock sync.Mutex
p.ApplyResourceChangeFn = testApplyFn
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
proposed := req.ProposedNewState
configsLock.Lock()
defer configsLock.Unlock()
key := proposed.GetAttr("key").AsString()
// This test was originally written using the legacy p.PlanResourceChangeFn interface,
// and so the assertions below expect an old-style ResourceConfig, which
// we'll construct via our shim for now to avoid rewriting all of the
// assertions.
configs[key] = req.ProposedNewState
retVals := make(map[string]cty.Value)
for it := proposed.ElementIterator(); it.Next(); {
idxVal, val := it.Element()
idx := idxVal.AsString()
switch idx {
case "id":
retVals[idx] = cty.UnknownVal(cty.String)
case "name":
retVals[idx] = cty.StringVal(key)
default:
retVals[idx] = val
}
}
return providers.PlanResourceChangeResponse{
PlannedState: cty.ObjectVal(retVals),
}
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_thing": {
Attributes: map[string]*configschema.Attribute{
"key": {Type: cty.String, Required: true},
"source_id": {Type: cty.String, Optional: true},
"source_name": {Type: cty.String, Optional: true},
"first_source_id": {Type: cty.String, Optional: true},
"first_source_name": {Type: cty.String, Optional: true},
"source_ids": {Type: cty.List(cty.String), Optional: true},
"source_names": {Type: cty.List(cty.String), Optional: true},
"source_ids_from_func": {Type: cty.List(cty.String), Optional: true},
"source_names_from_func": {Type: cty.List(cty.String), Optional: true},
"source_ids_wrapped": {Type: cty.List(cty.List(cty.String)), Optional: true},
"source_names_wrapped": {Type: cty.List(cty.List(cty.String)), Optional: true},
"id": {Type: cty.String, Computed: true},
"name": {Type: cty.String, Computed: true},
},
},
},
})
// First, apply with a count of 3
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"num": &InputValue{
Value: cty.NumberIntVal(3),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
checkConfig := func(key string, want cty.Value) {
configsLock.Lock()
defer configsLock.Unlock()
got, ok := configs[key]
if !ok {
t.Errorf("no config recorded for %s; expected a configuration", key)
return
}
t.Run("config for "+key, func(t *testing.T) {
for _, problem := range deep.Equal(got, want) {
t.Errorf(problem)
}
})
}
checkConfig("multi_count_var.0", cty.ObjectVal(map[string]cty.Value{
"source_id": cty.UnknownVal(cty.String),
"source_name": cty.StringVal("source.0"),
}))
checkConfig("multi_count_var.2", cty.ObjectVal(map[string]cty.Value{
"source_id": cty.UnknownVal(cty.String),
"source_name": cty.StringVal("source.2"),
}))
checkConfig("multi_count_derived.0", cty.ObjectVal(map[string]cty.Value{
"source_id": cty.UnknownVal(cty.String),
"source_name": cty.StringVal("source.0"),
}))
checkConfig("multi_count_derived.2", cty.ObjectVal(map[string]cty.Value{
"source_id": cty.UnknownVal(cty.String),
"source_name": cty.StringVal("source.2"),
}))
checkConfig("whole_splat", cty.ObjectVal(map[string]cty.Value{
"source_ids": cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
"source_names": cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
"source_ids_from_func": cty.UnknownVal(cty.String),
"source_names_from_func": cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
"source_ids_wrapped": cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
}),
"source_names_wrapped": cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
}),
"first_source_id": cty.UnknownVal(cty.String),
"first_source_name": cty.StringVal("source.0"),
}))
checkConfig("child.whole_splat", cty.ObjectVal(map[string]cty.Value{
"source_ids": cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
"source_names": cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
"source_ids_wrapped": cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
}),
"source_names_wrapped": cty.ListVal([]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("source.0"),
cty.StringVal("source.1"),
cty.StringVal("source.2"),
}),
}),
}))
t.Run("apply", func(t *testing.T) {
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("error during apply: %s", diags.Err())
}
want := map[string]interface{}{
"source_ids": []interface{}{"foo", "foo", "foo"},
"source_names": []interface{}{
"source.0",
"source.1",
"source.2",
},
}
got := map[string]interface{}{}
for k, s := range state.RootModule().OutputValues {
got[k] = hcl2shim.ConfigValueFromHCL2(s.Value)
}
if !reflect.DeepEqual(got, want) {
t.Errorf(
"wrong outputs\ngot: %s\nwant: %s",
spew.Sdump(got), spew.Sdump(want),
)
}
})
}
// Test that multi-var (splat) access is ordered by count, not by
// value.
func TestContext2Apply_multiVarOrder(t *testing.T) {
m := testModule(t, "apply-multi-var-order")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// First, apply with a count of 3
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
t.Logf("State: %s", state.String())
actual := state.RootModule().OutputValues["should-be-11"]
expected := cty.StringVal("index-11")
if actual == nil || actual.Value != expected {
t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected)
}
}
// Test that multi-var (splat) access is ordered by count, not by
// value, through interpolations.
func TestContext2Apply_multiVarOrderInterp(t *testing.T) {
m := testModule(t, "apply-multi-var-order-interp")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// First, apply with a count of 3
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
t.Logf("State: %s", state.String())
actual := state.RootModule().OutputValues["should-be-11"]
expected := cty.StringVal("baz-index-11")
if actual == nil || actual.Value != expected {
t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected)
}
}
// Based on GH-10440 where a graph edge wasn't properly being created
// between a modified resource and a count instance being destroyed.
func TestContext2Apply_multiVarCountDec(t *testing.T) {
var s *states.State
// First create resources. Nothing sneaky here.
{
m := testModule(t, "apply-multi-var-count-dec")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
log.Print("\n========\nStep 1 Plan\n========")
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"num": &InputValue{
Value: cty.NumberIntVal(2),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
log.Print("\n========\nStep 1 Apply\n========")
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
t.Logf("Step 1 state:\n%s", state)
s = state
}
// Decrease the count by 1 and verify that everything happens in the
// right order.
m := testModule(t, "apply-multi-var-count-dec")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
// Verify that aws_instance.bar is modified first and nothing
// else happens at the same time.
{
var checked bool
var called int32
var lock sync.Mutex
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
lock.Lock()
defer lock.Unlock()
if !req.PlannedState.IsNull() {
s := req.PlannedState.AsValueMap()
if ami, ok := s["ami"]; ok && !ami.IsNull() && ami.AsString() == "special" {
checked = true
// Sleep to allow parallel execution
time.Sleep(50 * time.Millisecond)
// Verify that called is 0 (dep not called)
if atomic.LoadInt32(&called) != 1 {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("nothing else should be called"))
return
}
}
}
atomic.AddInt32(&called, 1)
return testApplyFn(req)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
log.Print("\n========\nStep 2 Plan\n========")
plan, diags := ctx.Plan(m, s, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"num": &InputValue{
Value: cty.NumberIntVal(1),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
t.Logf("Step 2 plan:\n%s", legacyDiffComparisonString(plan.Changes))
log.Print("\n========\nStep 2 Apply\n========")
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
if !checked {
t.Error("apply never called")
}
}
}
// Test that we can resolve a multi-var (splat) for the first resource
// created in a non-root module, which happens when the module state doesn't
// exist yet.
// https://github.com/hashicorp/terraform/issues/14438
func TestContext2Apply_multiVarMissingState(t *testing.T) {
m := testModule(t, "apply-multi-var-missing-state")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_thing": {
Attributes: map[string]*configschema.Attribute{
"a_ids": {Type: cty.String, Optional: true},
"id": {Type: cty.String, Computed: true},
},
},
},
})
// First, apply with a count of 3
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
// Before the relevant bug was fixed, Terraform would panic during apply.
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply failed: %s", diags.Err())
}
// If we get here with no errors or panics then our test was successful.
}
func TestContext2Apply_outputOrphan(t *testing.T) {
m := testModule(t, "apply-output-orphan")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetOutputValue("foo", cty.StringVal("bar"), false)
root.SetOutputValue("bar", cty.StringVal("baz"), false)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputOrphanStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_outputOrphanModule(t *testing.T) {
m := testModule(t, "apply-output-orphan-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testTerraformApplyOutputOrphanModuleStr)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
// now apply with no module in the config, which should remove the
// remaining output
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
emptyConfig := configs.NewEmptyConfig()
// NOTE: While updating this test to pass the state in as a Plan argument,
// rather than into the testContext2 call above, it previously said
// State: state.DeepCopy(), which is a little weird since we just
// created "s" above as the result of the previous apply, but I've preserved
// it to avoid changing the flow of this test in case that's important
// for some reason.
plan, diags = ctx.Plan(emptyConfig, state.DeepCopy(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, emptyConfig)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if !state.Empty() {
t.Fatalf("wrong final state %s\nwant empty state", spew.Sdump(state))
}
}
func TestContext2Apply_providerComputedVar(t *testing.T) {
m := testModule(t, "apply-provider-computed")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
pTest := testProvider("test")
pTest.ApplyResourceChangeFn = testApplyFn
pTest.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("test"): testProviderFuncFixed(pTest),
},
})
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
val := req.Config.GetAttr("value")
if val.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found"))
return
}
return
}
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_providerConfigureDisabled(t *testing.T) {
m := testModule(t, "apply-provider-configure-disabled")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
val := req.Config.GetAttr("value")
if val.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found"))
}
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
if !p.ConfigureProviderCalled {
t.Fatal("configure never called")
}
}
func TestContext2Apply_provisionerModule(t *testing.T) {
m := testModule(t, "apply-provisioner-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr := testProvisioner()
pr.GetSchemaResponse = provisioners.GetSchemaResponse{
Provisioner: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
},
},
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerModuleStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
}
func TestContext2Apply_Provisioner_compute(t *testing.T) {
m := testModule(t, "apply-provisioner-compute")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
val := req.Config.GetAttr("command").AsString()
if val != "computed_value" {
t.Fatalf("bad value for foo: %q", val)
}
req.UIOutput.Output(fmt.Sprintf("Executing: %q", val))
return
}
h := new(MockHook)
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"value": &InputValue{
Value: cty.NumberIntVal(1),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
// Verify output was rendered
if !h.ProvisionOutputCalled {
t.Fatalf("ProvisionOutput hook not called")
}
if got, want := h.ProvisionOutputMessage, `Executing: "computed_value"`; got != want {
t.Errorf("expected output to be %q, but was %q", want, got)
}
}
func TestContext2Apply_provisionerCreateFail(t *testing.T) {
m := testModule(t, "apply-provisioner-fail-create")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
resp := testApplyFn(req)
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error"))
return resp
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags == nil {
t.Fatal("should error")
}
got := strings.TrimSpace(state.String())
want := strings.TrimSpace(testTerraformApplyProvisionerFailCreateStr)
if got != want {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", got, want)
}
}
func TestContext2Apply_provisionerCreateFailNoId(t *testing.T) {
m := testModule(t, "apply-provisioner-fail-create")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error"))
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags == nil {
t.Fatal("should error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerFailCreateNoIdStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_provisionerFail(t *testing.T) {
m := testModule(t, "apply-provisioner-fail")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr := testProvisioner()
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("EXPLOSION"))
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags == nil {
t.Fatal("should error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerFailStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-provisioner-fail-create-before")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("EXPLOSION"))
return
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","require_new":"abc"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("should error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerFailCreateBeforeDestroyStr)
if actual != expected {
t.Fatalf("expected:\n%s\n:got\n%s", expected, actual)
}
}
func TestContext2Apply_error_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-error-create-before")
p := testProvider("aws")
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar", "require_new": "abc","type":"aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("placeholder error from ApplyFn"))
return
}
p.PlanResourceChangeFn = testDiffFn
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("should have error")
}
if got, want := diags.Err().Error(), "placeholder error from ApplyFn"; got != want {
// We're looking for our artificial error from ApplyFn above, whose
// message is literally "placeholder error from ApplyFn".
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyErrorCreateBeforeDestroyStr)
if actual != expected {
t.Fatalf("wrong final state\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-error-create-before")
p := testProvider("aws")
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar", "require_new": "abc"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
// Fail the destroy!
if req.PlannedState.IsNull() {
resp.NewState = req.PriorState
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error"))
return
}
return testApplyFn(req)
}
p.PlanResourceChangeFn = testDiffFn
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("should have error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyErrorDestroyCreateBeforeDestroyStr)
if actual != expected {
t.Fatalf("bad: actual:\n%s\n\nexpected:\n%s", actual, expected)
}
}
func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-multi-depose-create-before-destroy")
p := testProvider("aws")
ps := map[addrs.Provider]providers.Factory{addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p)}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"require_new": {Type: cty.String, Optional: true},
"id": {Type: cty.String, Computed: true},
},
},
},
})
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.web").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: ps,
})
createdInstanceId := "bar"
// Create works
createFunc := func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
s := req.PlannedState.AsValueMap()
s["id"] = cty.StringVal(createdInstanceId)
resp.NewState = cty.ObjectVal(s)
return
}
// Destroy starts broken
destroyFunc := func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
resp.NewState = req.PriorState
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("destroy failed"))
return
}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
if req.PlannedState.IsNull() {
return destroyFunc(req)
} else {
return createFunc(req)
}
}
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"require_new": &InputValue{
Value: cty.StringVal("yes"),
},
},
})
assertNoErrors(t, diags)
// Destroy is broken, so even though CBD successfully replaces the instance,
// we'll have to save the Deposed instance to destroy later
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("should have error")
}
checkStateString(t, state, `
aws_instance.web: (1 deposed)
ID = bar
provider = provider["registry.terraform.io/hashicorp/aws"]
require_new = yes
Deposed ID 1 = foo
`)
createdInstanceId = "baz"
ctx = testContext2(t, &ContextOpts{
Providers: ps,
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"require_new": &InputValue{
Value: cty.StringVal("baz"),
},
},
})
assertNoErrors(t, diags)
// We're replacing the primary instance once again. Destroy is _still_
// broken, so the Deposed list gets longer
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("should have error")
}
// For this one we can't rely on checkStateString because its result is
// not deterministic when multiple deposed objects are present. Instead,
// we will probe the state object directly.
{
is := state.RootModule().Resources["aws_instance.web"].Instances[addrs.NoKey]
if is.Current == nil {
t.Fatalf("no current object for aws_instance web; should have one")
}
if !bytes.Contains(is.Current.AttrsJSON, []byte("baz")) {
t.Fatalf("incorrect current object attrs %s; want id=baz", is.Current.AttrsJSON)
}
if got, want := len(is.Deposed), 2; got != want {
t.Fatalf("wrong number of deposed instances %d; want %d", got, want)
}
var foos, bars int
for _, obj := range is.Deposed {
if bytes.Contains(obj.AttrsJSON, []byte("foo")) {
foos++
}
if bytes.Contains(obj.AttrsJSON, []byte("bar")) {
bars++
}
}
if got, want := foos, 1; got != want {
t.Fatalf("wrong number of deposed instances with id=foo %d; want %d", got, want)
}
if got, want := bars, 1; got != want {
t.Fatalf("wrong number of deposed instances with id=bar %d; want %d", got, want)
}
}
// Destroy partially fixed!
destroyFunc = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
s := req.PriorState.AsValueMap()
id := s["id"].AsString()
if id == "foo" || id == "baz" {
resp.NewState = cty.NullVal(req.PriorState.Type())
} else {
resp.NewState = req.PriorState
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("destroy partially failed"))
}
return
}
createdInstanceId = "qux"
ctx = testContext2(t, &ContextOpts{
Providers: ps,
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"require_new": &InputValue{
Value: cty.StringVal("qux"),
},
},
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
// Expect error because 1/2 of Deposed destroys failed
if !diags.HasErrors() {
t.Fatal("should have error")
}
// foo and baz are now gone, bar sticks around
checkStateString(t, state, `
aws_instance.web: (1 deposed)
ID = qux
provider = provider["registry.terraform.io/hashicorp/aws"]
require_new = qux
Deposed ID 1 = bar
`)
// Destroy working fully!
destroyFunc = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
resp.NewState = cty.NullVal(req.PriorState.Type())
return
}
createdInstanceId = "quux"
ctx = testContext2(t, &ContextOpts{
Providers: ps,
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"require_new": &InputValue{
Value: cty.StringVal("quux"),
},
},
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal("should not have error:", diags.Err())
}
// And finally the state is clean
checkStateString(t, state, `
aws_instance.web:
ID = quux
provider = provider["registry.terraform.io/hashicorp/aws"]
require_new = quux
`)
}
// Verify that a normal provisioner with on_failure "continue" set won't
// taint the resource and continues executing.
func TestContext2Apply_provisionerFailContinue(t *testing.T) {
m := testModule(t, "apply-provisioner-fail-continue")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error"))
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = bar
type = aws_instance
`)
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
}
// Verify that a normal provisioner with on_failure "continue" records
// the error with the hook.
func TestContext2Apply_provisionerFailContinueHook(t *testing.T) {
h := new(MockHook)
m := testModule(t, "apply-provisioner-fail-continue")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error"))
return
}
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
if !h.PostProvisionInstanceStepCalled {
t.Fatal("PostProvisionInstanceStep not called")
}
if h.PostProvisionInstanceStepErrorArg == nil {
t.Fatal("should have error")
}
}
func TestContext2Apply_provisionerDestroy(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
val := req.Config.GetAttr("command").AsString()
if val != "destroy a bar" {
t.Fatalf("bad value for foo: %q", val)
}
return
}
state := states.NewState()
root := state.RootModule()
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","foo":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `<no state>`)
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
}
// Verify that on destroy provisioner failure, nothing happens to the instance
func TestContext2Apply_provisionerDestroyFail(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error"))
return
}
state := states.NewState()
root := state.RootModule()
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","foo":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags == nil {
t.Fatal("should error")
}
checkStateString(t, state, `
aws_instance.foo["a"]:
ID = bar
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = bar
`)
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
}
// Verify that on destroy provisioner failure with "continue" that
// we continue to the next provisioner.
func TestContext2Apply_provisionerDestroyFailContinue(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy-continue")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
var l sync.Mutex
var calls []string
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
val := req.Config.GetAttr("command")
if val.IsNull() {
t.Fatalf("bad value for foo: %#v", val)
}
l.Lock()
defer l.Unlock()
calls = append(calls, val.AsString())
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error"))
return
}
state := states.NewState()
root := state.RootModule()
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `<no state>`)
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
expected := []string{"one", "two"}
if !reflect.DeepEqual(calls, expected) {
t.Fatalf("wrong commands\ngot: %#v\nwant: %#v", calls, expected)
}
}
// Verify that on destroy provisioner failure with "continue" that
// we continue to the next provisioner. But if the next provisioner defines
// to fail, then we fail after running it.
func TestContext2Apply_provisionerDestroyFailContinueFail(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy-fail")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
var l sync.Mutex
var calls []string
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
val := req.Config.GetAttr("command")
if val.IsNull() {
t.Fatalf("bad value for foo: %#v", val)
}
l.Lock()
defer l.Unlock()
calls = append(calls, val.AsString())
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error"))
return
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags == nil {
t.Fatal("apply succeeded; wanted error from second provisioner")
}
checkStateString(t, state, `
aws_instance.foo:
ID = bar
provider = provider["registry.terraform.io/hashicorp/aws"]
`)
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
expected := []string{"one", "two"}
if !reflect.DeepEqual(calls, expected) {
t.Fatalf("bad: %#v", calls)
}
}
// Verify destroy provisioners are not run for tainted instances.
func TestContext2Apply_provisionerDestroyTainted(t *testing.T) {
m := testModule(t, "apply-provisioner-destroy")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
destroyCalled := false
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
expected := "create a b"
val := req.Config.GetAttr("command")
if val.AsString() != expected {
t.Fatalf("bad value for command: %#v", val)
}
return
}
state := states.NewState()
root := state.RootModule()
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"input": &InputValue{
Value: cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("b"),
}),
SourceType: ValueFromInput,
},
},
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.foo["a"]:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = bar
type = aws_instance
`)
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
if destroyCalled {
t.Fatal("destroy should not be called")
}
}
func TestContext2Apply_provisionerResourceRef(t *testing.T) {
m := testModule(t, "apply-provisioner-resource-ref")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr := testProvisioner()
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
val := req.Config.GetAttr("command")
if val.AsString() != "2" {
t.Fatalf("bad value for command: %#v", val)
}
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerResourceRefStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
}
func TestContext2Apply_provisionerSelfRef(t *testing.T) {
m := testModule(t, "apply-provisioner-self-ref")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
val := req.Config.GetAttr("command")
if val.AsString() != "bar" {
t.Fatalf("bad value for command: %#v", val)
}
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerSelfRefStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
}
func TestContext2Apply_provisionerMultiSelfRef(t *testing.T) {
var lock sync.Mutex
commands := make([]string, 0, 5)
m := testModule(t, "apply-provisioner-multi-self-ref")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
lock.Lock()
defer lock.Unlock()
val := req.Config.GetAttr("command")
if val.IsNull() {
t.Fatalf("bad value for command: %#v", val)
}
commands = append(commands, val.AsString())
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerMultiSelfRefStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
// Verify our result
sort.Strings(commands)
expectedCommands := []string{"number 0", "number 1", "number 2"}
if !reflect.DeepEqual(commands, expectedCommands) {
t.Fatalf("bad: %#v", commands)
}
}
func TestContext2Apply_provisionerMultiSelfRefSingle(t *testing.T) {
var lock sync.Mutex
order := make([]string, 0, 5)
m := testModule(t, "apply-provisioner-multi-self-ref-single")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
lock.Lock()
defer lock.Unlock()
val := req.Config.GetAttr("order")
if val.IsNull() {
t.Fatalf("no val for order")
}
order = append(order, val.AsString())
return
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerMultiSelfRefSingleStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
// Verify our result
sort.Strings(order)
expectedOrder := []string{"0", "1", "2"}
if !reflect.DeepEqual(order, expectedOrder) {
t.Fatalf("bad: %#v", order)
}
}
func TestContext2Apply_provisionerExplicitSelfRef(t *testing.T) {
m := testModule(t, "apply-provisioner-explicit-self-ref")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
val := req.Config.GetAttr("command")
if val.IsNull() || val.AsString() != "bar" {
t.Fatalf("bad value for command: %#v", val)
}
return
}
var state *states.State
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner not invoked")
}
}
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `<no state>`)
}
}
func TestContext2Apply_provisionerForEachSelfRef(t *testing.T) {
m := testModule(t, "apply-provisioner-for-each-self")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
val := req.Config.GetAttr("command")
if val.IsNull() {
t.Fatalf("bad value for command: %#v", val)
}
return resp
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
}
// Provisioner should NOT run on a diff, only create
func TestContext2Apply_Provisioner_Diff(t *testing.T) {
m := testModule(t, "apply-provisioner-diff")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("apply failed")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerDiffStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner was not called on first apply")
}
pr.ProvisionResourceCalled = false
// Change the state to force a diff
mod := state.RootModule()
obj := mod.Resources["aws_instance.bar"].Instances[addrs.NoKey].Current
var attrs map[string]interface{}
err := json.Unmarshal(obj.AttrsJSON, &attrs)
if err != nil {
t.Fatal(err)
}
attrs["foo"] = "baz"
obj.AttrsJSON, err = json.Marshal(attrs)
if err != nil {
t.Fatal(err)
}
// Re-create context with state
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state2, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("apply failed")
}
actual = strings.TrimSpace(state2.String())
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was NOT invoked
if pr.ProvisionResourceCalled {
t.Fatalf("provisioner was called on second apply; should not have been")
}
}
func TestContext2Apply_outputDiffVars(t *testing.T) {
m := testModule(t, "apply-good")
p := testProvider("aws")
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.baz").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
p.PlanResourceChangeFn = testDiffFn
//func(info *InstanceInfo, s *InstanceState, rc *ResourceConfig) (*InstanceDiff, error) {
// d := &InstanceDiff{
// Attributes: map[string]*ResourceAttrDiff{},
// }
// if new, ok := rc.Get("value"); ok {
// d.Attributes["value"] = &ResourceAttrDiff{
// New: new.(string),
// }
// }
// if new, ok := rc.Get("foo"); ok {
// d.Attributes["foo"] = &ResourceAttrDiff{
// New: new.(string),
// }
// } else if rc.IsComputed("foo") {
// d.Attributes["foo"] = &ResourceAttrDiff{
// NewComputed: true,
// Type: DiffAttrOutput, // This doesn't actually really do anything anymore, but this test originally set it.
// }
// }
// if new, ok := rc.Get("num"); ok {
// d.Attributes["num"] = &ResourceAttrDiff{
// New: fmt.Sprintf("%#v", new),
// }
// }
// return d, nil
//}
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
}
func TestContext2Apply_destroyX(t *testing.T) {
m := testModule(t, "apply-destroy")
h := new(HookRecordApplyOrder)
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Next, plan and apply a destroy operation
h.Active = true
ctx = testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Test that things were destroyed
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyDestroyStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Test that things were destroyed _in the right order_
expected2 := []string{"aws_instance.bar", "aws_instance.foo"}
actual2 := h.IDs
if !reflect.DeepEqual(actual2, expected2) {
t.Fatalf("expected: %#v\n\ngot:%#v", expected2, actual2)
}
}
func TestContext2Apply_destroyOrder(t *testing.T) {
m := testModule(t, "apply-destroy")
h := new(HookRecordApplyOrder)
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
t.Logf("State 1: %s", state)
// Next, plan and apply a destroy
h.Active = true
ctx = testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Test that things were destroyed
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyDestroyStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Test that things were destroyed _in the right order_
expected2 := []string{"aws_instance.bar", "aws_instance.foo"}
actual2 := h.IDs
if !reflect.DeepEqual(actual2, expected2) {
t.Fatalf("expected: %#v\n\ngot:%#v", expected2, actual2)
}
}
// https://github.com/hashicorp/terraform/issues/2767
func TestContext2Apply_destroyModulePrefix(t *testing.T) {
m := testModule(t, "apply-destroy-module-resource-prefix")
h := new(MockHook)
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Verify that we got the apply info correct
if v := h.PreApplyAddr.String(); v != "module.child.aws_instance.foo" {
t.Fatalf("bad: %s", v)
}
// Next, plan and apply a destroy operation and reset the hook
h = new(MockHook)
ctx = testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Test that things were destroyed
if v := h.PreApplyAddr.String(); v != "module.child.aws_instance.foo" {
t.Fatalf("bad: %s", v)
}
}
func TestContext2Apply_destroyNestedModule(t *testing.T) {
m := testModule(t, "apply-destroy-nested-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
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"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Test that things were destroyed
actual := strings.TrimSpace(s.String())
if actual != "<no state>" {
t.Fatalf("expected no state, got: %s", actual)
}
}
func TestContext2Apply_destroyDeeplyNestedModule(t *testing.T) {
m := testModule(t, "apply-destroy-deeply-nested-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
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"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Test that things were destroyed
if !s.Empty() {
t.Fatalf("wrong final state %s\nwant empty state", spew.Sdump(s))
}
}
// https://github.com/hashicorp/terraform/issues/5440
func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) {
m, snap := testModuleWithSnapshot(t, "apply-destroy-module-with-attrs")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
var state *states.State
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("plan diags: %s", diags.Err())
} else {
t.Logf("Step 1 plan: %s", legacyDiffComparisonString(plan.Changes))
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errs: %s", diags.Err())
}
t.Logf("Step 1 state: %s", state)
}
h := new(HookRecordApplyOrder)
h.Active = true
{
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatalf("destroy plan err: %s", diags.Err())
}
t.Logf("Step 2 plan: %s", legacyDiffComparisonString(plan.Changes))
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers = map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
}
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("destroy apply err: %s", diags.Err())
}
t.Logf("Step 2 state: %s", state)
}
//Test that things were destroyed
if state.HasManagedResourceInstanceObjects() {
t.Fatal("expected empty state, got:", state)
}
}
func TestContext2Apply_destroyWithModuleVariableAndCount(t *testing.T) {
m, snap := testModuleWithSnapshot(t, "apply-destroy-mod-var-and-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
var state *states.State
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply err: %s", diags.Err())
}
}
h := new(HookRecordApplyOrder)
h.Active = true
{
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatalf("destroy plan err: %s", diags.Err())
}
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers =
map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
}
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("destroy apply err: %s", diags.Err())
}
}
//Test that things were destroyed
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`
<no state>`)
if actual != expected {
t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual)
}
}
func TestContext2Apply_destroyTargetWithModuleVariableAndCount(t *testing.T) {
m := testModule(t, "apply-destroy-mod-var-and-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
var state *states.State
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply err: %s", diags.Err())
}
}
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Child("child", addrs.NoKey),
},
})
if diags.HasErrors() {
t.Fatalf("plan err: %s", diags)
}
if len(diags) != 1 {
// Should have one warning that -target is in effect.
t.Fatalf("got %d diagnostics in plan; want 1", len(diags))
}
if got, want := diags[0].Severity(), tfdiags.Warning; got != want {
t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
}
if got, want := diags[0].Description().Summary, "Resource targeting is in effect"; got != want {
t.Errorf("wrong diagnostic summary %#v; want %#v", got, want)
}
// Destroy, targeting the module explicitly
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("destroy apply err: %s", diags)
}
if len(diags) != 1 {
t.Fatalf("got %d diagnostics; want 1", len(diags))
}
if got, want := diags[0].Severity(), tfdiags.Warning; got != want {
t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
}
if got, want := diags[0].Description().Summary, "Applied changes may be incomplete"; got != want {
t.Errorf("wrong diagnostic summary %#v; want %#v", got, want)
}
}
//Test that things were destroyed
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`<no state>`)
if actual != expected {
t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual)
}
}
func TestContext2Apply_destroyWithModuleVariableAndCountNested(t *testing.T) {
m, snap := testModuleWithSnapshot(t, "apply-destroy-mod-var-and-count-nested")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
var state *states.State
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply err: %s", diags.Err())
}
}
h := new(HookRecordApplyOrder)
h.Active = true
{
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatalf("destroy plan err: %s", diags.Err())
}
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers =
map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
}
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("destroy apply err: %s", diags.Err())
}
}
//Test that things were destroyed
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`
<no state>`)
if actual != expected {
t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual)
}
}
func TestContext2Apply_destroyOutputs(t *testing.T) {
m := testModule(t, "apply-destroy-outputs")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
// add the required id
m := req.Config.AsValueMap()
m["id"] = cty.StringVal("foo")
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(m),
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// Next, plan and apply a destroy operation
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) > 0 {
t.Fatalf("expected no resources, got: %#v", mod)
}
// destroying again should produce no errors
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatal(diags.Err())
}
}
func TestContext2Apply_destroyOrphan(t *testing.T) {
m := testModule(t, "apply-error")
p := testProvider("aws")
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.baz").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
p.PlanResourceChangeFn = testDiffFn
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := s.RootModule()
if _, ok := mod.Resources["aws_instance.baz"]; ok {
t.Fatalf("bad: %#v", mod.Resources)
}
}
func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) {
m := testModule(t, "apply-destroy-provisioner")
p := testProvider("aws")
pr := testProvisioner()
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if pr.ProvisionResourceCalled {
t.Fatal("provisioner should not be called")
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace("<no state>")
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_error(t *testing.T) {
errored := false
m := testModule(t, "apply-error")
p := testProvider("aws")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
if errored {
resp.NewState = req.PlannedState
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error"))
return
}
errored = true
return testApplyFn(req)
}
p.PlanResourceChangeFn = testDiffFn
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags == nil {
t.Fatal("should have error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyErrorStr)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
}
func TestContext2Apply_errorDestroy(t *testing.T) {
m := testModule(t, "empty")
p := testProvider("test")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_thing": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true},
},
},
},
})
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
// Should actually be called for this test, because Terraform Core
// constructs the plan for a destroy operation itself.
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
// The apply (in this case, a destroy) always fails, so we can verify
// that the object stays in the state after a destroy fails even though
// we aren't returning a new state object here.
return providers.ApplyResourceChangeResponse{
Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Errorf("failed")),
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
state := states.BuildState(func(ss *states.SyncState) {
ss.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"baz"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("should have error")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`
test_thing.foo:
ID = baz
provider = provider["registry.terraform.io/hashicorp/test"]
`) // test_thing.foo is still here, even though provider returned no new state along with its error
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
}
func TestContext2Apply_errorCreateInvalidNew(t *testing.T) {
m := testModule(t, "apply-error")
p := testProvider("aws")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
"foo": {Type: cty.String, Optional: true},
},
},
},
})
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
// We're intentionally returning an inconsistent new state here
// because we want to test that Terraform ignores the inconsistency
// when accompanied by another error.
return providers.ApplyResourceChangeResponse{
NewState: cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("wrong wrong wrong wrong"),
"foo": cty.StringVal("absolutely brimming over with wrongability"),
}),
Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Errorf("forced error")),
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags == nil {
t.Fatal("should have error")
}
if got, want := len(diags), 1; got != want {
// There should be no additional diagnostics generated by Terraform's own eval logic,
// because the provider's own error supersedes them.
t.Errorf("wrong number of diagnostics %d; want %d\n%s", got, want, diags.Err())
}
if got, want := diags.Err().Error(), "forced error"; !strings.Contains(got, want) {
t.Errorf("returned error does not contain %q, but it should\n%s", want, diags.Err())
}
if got, want := len(state.RootModule().Resources), 2; got != want {
t.Errorf("%d resources in state before prune; should have %d\n%s", got, want, spew.Sdump(state))
}
state.PruneResourceHusks() // aws_instance.bar with no instances gets left behind when we bail out, but that's okay
if got, want := len(state.RootModule().Resources), 1; got != want {
t.Errorf("%d resources in state after prune; should have only one (aws_instance.foo, tainted)\n%s", got, spew.Sdump(state))
}
}
func TestContext2Apply_errorUpdateNullNew(t *testing.T) {
m := testModule(t, "apply-error")
p := testProvider("aws")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
"foo": {Type: cty.String, Optional: true},
},
},
},
})
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
// We're intentionally returning no NewState here because we want to
// test that Terraform retains the prior state, rather than treating
// the returned null as "no state" (object deleted).
return providers.ApplyResourceChangeResponse{
Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Errorf("forced error")),
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
state := states.BuildState(func(ss *states.SyncState) {
ss.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"value":"old"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("should have error")
}
if got, want := len(diags), 1; got != want {
// There should be no additional diagnostics generated by Terraform's own eval logic,
// because the provider's own error supersedes them.
t.Errorf("wrong number of diagnostics %d; want %d\n%s", got, want, diags.Err())
}
if got, want := diags.Err().Error(), "forced error"; !strings.Contains(got, want) {
t.Errorf("returned error does not contain %q, but it should\n%s", want, diags.Err())
}
state.PruneResourceHusks()
if got, want := len(state.RootModule().Resources), 1; got != want {
t.Fatalf("%d resources in state; should have only one (aws_instance.foo, unmodified)\n%s", got, spew.Sdump(state))
}
is := state.ResourceInstance(addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance))
if is == nil {
t.Fatalf("aws_instance.foo is not in the state after apply")
}
if got, want := is.Current.AttrsJSON, []byte(`"old"`); !bytes.Contains(got, want) {
t.Fatalf("incorrect attributes for aws_instance.foo\ngot: %s\nwant: JSON containing %s\n\n%s", got, want, spew.Sdump(is))
}
}
func TestContext2Apply_errorPartial(t *testing.T) {
errored := false
m := testModule(t, "apply-error")
p := testProvider("aws")
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","type":"aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
if errored {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error"))
return
}
errored = true
return testApplyFn(req)
}
p.PlanResourceChangeFn = testDiffFn
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags == nil {
t.Fatal("should have error")
}
mod := s.RootModule()
if len(mod.Resources) != 2 {
t.Fatalf("bad: %#v", mod.Resources)
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testTerraformApplyErrorPartialStr)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
}
func TestContext2Apply_hook(t *testing.T) {
m := testModule(t, "apply-good")
h := new(MockHook)
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
if !h.PreApplyCalled {
t.Fatal("should be called")
}
if !h.PostApplyCalled {
t.Fatal("should be called")
}
if !h.PostStateUpdateCalled {
t.Fatalf("should call post state update")
}
}
func TestContext2Apply_hookOrphan(t *testing.T) {
m := testModule(t, "apply-blank")
h := new(MockHook)
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
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"]`),
)
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
if !h.PreApplyCalled {
t.Fatal("should be called")
}
if !h.PostApplyCalled {
t.Fatal("should be called")
}
if !h.PostStateUpdateCalled {
t.Fatalf("should call post state update")
}
}
func TestContext2Apply_idAttr(t *testing.T) {
m := testModule(t, "apply-idattr")
p := testProvider("aws")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
mod := state.RootModule()
rs, ok := mod.Resources["aws_instance.foo"]
if !ok {
t.Fatal("not in state")
}
var attrs map[string]interface{}
err := json.Unmarshal(rs.Instances[addrs.NoKey].Current.AttrsJSON, &attrs)
if err != nil {
t.Fatal(err)
}
if got, want := attrs["id"], "foo"; got != want {
t.Fatalf("wrong id\ngot: %#v\nwant: %#v", got, want)
}
}
func TestContext2Apply_outputBasic(t *testing.T) {
m := testModule(t, "apply-output")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_outputAdd(t *testing.T) {
m1 := testModule(t, "apply-output-add-before")
p1 := testProvider("aws")
p1.ApplyResourceChangeFn = testApplyFn
p1.PlanResourceChangeFn = testDiffFn
ctx1 := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p1),
},
})
plan1, diags := ctx1.Plan(m1, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state1, diags := ctx1.Apply(plan1, m1)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
m2 := testModule(t, "apply-output-add-after")
p2 := testProvider("aws")
p2.ApplyResourceChangeFn = testApplyFn
p2.PlanResourceChangeFn = testDiffFn
ctx2 := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p2),
},
})
plan2, diags := ctx1.Plan(m2, state1, DefaultPlanOpts)
assertNoErrors(t, diags)
state2, diags := ctx2.Apply(plan2, m2)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state2.String())
expected := strings.TrimSpace(testTerraformApplyOutputAddStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_outputList(t *testing.T) {
m := testModule(t, "apply-output-list")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputListStr)
if actual != expected {
t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual)
}
}
func TestContext2Apply_outputMulti(t *testing.T) {
m := testModule(t, "apply-output-multi")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputMultiStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_outputMultiIndex(t *testing.T) {
m := testModule(t, "apply-output-multi-index")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyOutputMultiIndexStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_taintX(t *testing.T) {
m := testModule(t, "apply-taint")
p := testProvider("aws")
// destroyCount tests against regression of
// https://github.com/hashicorp/terraform/issues/1056
var destroyCount = int32(0)
var once sync.Once
simulateProviderDelay := func() {
time.Sleep(10 * time.Millisecond)
}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
once.Do(simulateProviderDelay)
if req.PlannedState.IsNull() {
atomic.AddInt32(&destroyCount, 1)
}
return testApplyFn(req)
}
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"baz","num": "2", "type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf("plan: %s", legacyDiffComparisonString(plan.Changes))
}
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testTerraformApplyTaintStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
if destroyCount != 1 {
t.Fatalf("Expected 1 destroy, got %d", destroyCount)
}
}
func TestContext2Apply_taintDep(t *testing.T) {
m := testModule(t, "apply-taint-dep")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"baz","num": "2", "type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","num": "2", "type": "aws_instance", "foo": "baz"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("aws_instance.foo")},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf("plan: %s", legacyDiffComparisonString(plan.Changes))
}
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testTerraformApplyTaintDepStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
}
func TestContext2Apply_taintDepRequiresNew(t *testing.T) {
m := testModule(t, "apply-taint-dep-requires-new")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"baz","num": "2", "type": "aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","num": "2", "type": "aws_instance", "foo": "baz"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("aws_instance.foo")},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf("plan: %s", legacyDiffComparisonString(plan.Changes))
}
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(testTerraformApplyTaintDepRequireNewStr)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
}
func TestContext2Apply_targeted(t *testing.T) {
m := testModule(t, "apply-targeted")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "foo",
),
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) != 1 {
t.Fatalf("expected 1 resource, got: %#v", mod.Resources)
}
checkStateString(t, state, `
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
num = 2
type = aws_instance
`)
}
func TestContext2Apply_targetedCount(t *testing.T) {
m := testModule(t, "apply-targeted-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "foo",
),
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.foo.0:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.foo.1:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.foo.2:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`)
}
func TestContext2Apply_targetedCountIndex(t *testing.T) {
m := testModule(t, "apply-targeted-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(1),
),
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.foo.1:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`)
}
func TestContext2Apply_targetedDestroy(t *testing.T) {
m := testModule(t, "destroy-targeted")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetOutputValue("out", cty.StringVal("bar"), false)
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.b").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-bcd345"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
if diags := ctx.Validate(m); diags.HasErrors() {
t.Fatalf("validate errors: %s", diags.Err())
}
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "a",
),
},
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) != 0 {
t.Fatalf("expected 0 resources, got: %#v", mod.Resources)
}
// the root output should not get removed; only the targeted resource.
//
// Note: earlier versions of this test expected 0 outputs, but it turns out
// that was because Validate - not apply or destroy - removed the output
// (which depends on the targeted resource) from state. That version of this
// test did not match actual terraform behavior: the output remains in
// state.
//
// TODO: Future refactoring may enable us to remove the output from state in
// this case, and that would be Just Fine - this test can be modified to
// expect 0 outputs.
if len(mod.OutputValues) != 1 {
t.Fatalf("expected 1 outputs, got: %#v", mod.OutputValues)
}
// the module instance should remain
mod = state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey))
if len(mod.Resources) != 1 {
t.Fatalf("expected 1 resources, got: %#v", mod.Resources)
}
}
func TestContext2Apply_targetedDestroyCountDeps(t *testing.T) {
m := testModule(t, "apply-destroy-targeted-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-bcd345"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("aws_instance.foo")},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "foo",
),
},
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `<no state>`)
}
// https://github.com/hashicorp/terraform/issues/4462
func TestContext2Apply_targetedDestroyModule(t *testing.T) {
m := testModule(t, "apply-targeted-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-bcd345"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-bcd345"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Child("child", addrs.NoKey).Resource(
addrs.ManagedResourceMode, "aws_instance", "foo",
),
},
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.bar:
ID = i-abc123
provider = provider["registry.terraform.io/hashicorp/aws"]
aws_instance.foo:
ID = i-bcd345
provider = provider["registry.terraform.io/hashicorp/aws"]
module.child:
aws_instance.bar:
ID = i-abc123
provider = provider["registry.terraform.io/hashicorp/aws"]
`)
}
func TestContext2Apply_targetedDestroyCountIndex(t *testing.T) {
m := testModule(t, "apply-targeted-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
foo := &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-bcd345"}`),
}
bar := &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[0]").Resource,
foo,
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[1]").Resource,
foo,
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[2]").Resource,
foo,
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar[0]").Resource,
bar,
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar[1]").Resource,
bar,
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar[2]").Resource,
bar,
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(2),
),
addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "bar", addrs.IntKey(1),
),
},
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.bar.0:
ID = i-abc123
provider = provider["registry.terraform.io/hashicorp/aws"]
aws_instance.bar.2:
ID = i-abc123
provider = provider["registry.terraform.io/hashicorp/aws"]
aws_instance.foo.0:
ID = i-bcd345
provider = provider["registry.terraform.io/hashicorp/aws"]
aws_instance.foo.1:
ID = i-bcd345
provider = provider["registry.terraform.io/hashicorp/aws"]
`)
}
func TestContext2Apply_targetedModule(t *testing.T) {
m := testModule(t, "apply-targeted-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Child("child", addrs.NoKey),
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey))
if mod == nil {
t.Fatalf("no child module found in the state!\n\n%#v", state)
}
if len(mod.Resources) != 2 {
t.Fatalf("expected 2 resources, got: %#v", mod.Resources)
}
checkStateString(t, state, `
<no state>
module.child:
aws_instance.bar:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
num = 2
type = aws_instance
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
num = 2
type = aws_instance
`)
}
// GH-1858
func TestContext2Apply_targetedModuleDep(t *testing.T) {
m := testModule(t, "apply-targeted-module-dep")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "foo",
),
},
})
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf("Diff: %s", legacyDiffComparisonString(plan.Changes))
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = foo
type = aws_instance
Dependencies:
module.child.aws_instance.mod
module.child:
aws_instance.mod:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
Outputs:
output = foo
`)
}
// GH-10911 untargeted outputs should not be in the graph, and therefore
// not execute.
func TestContext2Apply_targetedModuleUnrelatedOutputs(t *testing.T) {
m := testModule(t, "apply-targeted-module-unrelated-outputs")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
_ = state.EnsureModule(addrs.RootModuleInstance.Child("child2", addrs.NoKey))
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Child("child2", addrs.NoKey),
},
})
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// - module.child1's instance_id output is dropped because we don't preserve
// non-root module outputs between runs (they can be recalculated from config)
// - module.child2's instance_id is updated because its dependency is updated
// - child2_id is updated because if its transitive dependency via module.child2
checkStateString(t, s, `
<no state>
Outputs:
child2_id = foo
module.child2:
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
Outputs:
instance_id = foo
`)
}
func TestContext2Apply_targetedModuleResource(t *testing.T) {
m := testModule(t, "apply-targeted-module-resource")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Child("child", addrs.NoKey).Resource(
addrs.ManagedResourceMode, "aws_instance", "foo",
),
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey))
if mod == nil || len(mod.Resources) != 1 {
t.Fatalf("expected 1 resource, got: %#v", mod)
}
checkStateString(t, state, `
<no state>
module.child:
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
num = 2
type = aws_instance
`)
}
func TestContext2Apply_targetedResourceOrphanModule(t *testing.T) {
m := testModule(t, "apply-targeted-resource-orphan-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
child := state.EnsureModule(addrs.RootModuleInstance.Child("parent", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"type":"aws_instance"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "foo",
),
},
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_unknownAttribute(t *testing.T) {
m := testModule(t, "apply-unknown")
p := testProvider("aws")
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
resp = testDiffFn(req)
planned := resp.PlannedState.AsValueMap()
planned["unknown"] = cty.UnknownVal(cty.String)
resp.PlannedState = cty.ObjectVal(planned)
return resp
}
p.ApplyResourceChangeFn = testApplyFn
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"num": {Type: cty.Number, Optional: true},
"unknown": {Type: cty.String, Computed: true},
"type": {Type: cty.String, Computed: true},
},
},
},
})
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Error("should error, because attribute 'unknown' is still unknown after apply")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyUnknownAttrStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_unknownAttributeInterpolate(t *testing.T) {
m := testModule(t, "apply-unknown-interpolate")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
if _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts); diags == nil {
t.Fatal("should error")
}
}
func TestContext2Apply_vars(t *testing.T) {
fixture := contextFixtureApplyVars(t)
opts := fixture.ContextOpts()
ctx := testContext2(t, opts)
m := fixture.Config
diags := ctx.Validate(m)
if len(diags) != 0 {
t.Fatalf("bad: %s", diags.ErrWithWarnings())
}
variables := InputValues{
"foo": &InputValue{
Value: cty.StringVal("us-east-1"),
SourceType: ValueFromCaller,
},
"test_list": &InputValue{
Value: cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("World"),
}),
SourceType: ValueFromCaller,
},
"test_map": &InputValue{
Value: cty.MapVal(map[string]cty.Value{
"Hello": cty.StringVal("World"),
"Foo": cty.StringVal("Bar"),
"Baz": cty.StringVal("Foo"),
}),
SourceType: ValueFromCaller,
},
"amis": &InputValue{
Value: cty.MapVal(map[string]cty.Value{
"us-east-1": cty.StringVal("override"),
}),
SourceType: ValueFromCaller,
},
}
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: variables,
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
got := strings.TrimSpace(state.String())
want := strings.TrimSpace(testTerraformApplyVarsStr)
if got != want {
t.Errorf("wrong result\n\ngot:\n%s\n\nwant:\n%s", got, want)
}
}
func TestContext2Apply_varsEnv(t *testing.T) {
fixture := contextFixtureApplyVarsEnv(t)
opts := fixture.ContextOpts()
ctx := testContext2(t, opts)
m := fixture.Config
diags := ctx.Validate(m)
if len(diags) != 0 {
t.Fatalf("bad: %s", diags.ErrWithWarnings())
}
variables := InputValues{
"string": &InputValue{
Value: cty.StringVal("baz"),
SourceType: ValueFromEnvVar,
},
"list": &InputValue{
Value: cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("World"),
}),
SourceType: ValueFromEnvVar,
},
"map": &InputValue{
Value: cty.MapVal(map[string]cty.Value{
"Hello": cty.StringVal("World"),
"Foo": cty.StringVal("Bar"),
"Baz": cty.StringVal("Foo"),
}),
SourceType: ValueFromEnvVar,
},
}
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: variables,
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyVarsEnvStr)
if actual != expected {
t.Errorf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_createBefore_depends(t *testing.T) {
m := testModule(t, "apply-depends-create-before")
h := new(HookRecordApplyOrder)
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "web",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","require_new":"ami-old"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "lb",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"baz","instance":"bar"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "web",
},
Module: addrs.RootModule,
},
},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("plan failed")
} else {
t.Logf("plan:\n%s", legacyDiffComparisonString(plan.Changes))
}
h.Active = true
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("apply failed")
}
mod := state.RootModule()
if len(mod.Resources) < 2 {
t.Logf("state after apply:\n%s", state.String())
t.Fatalf("only %d resources in root module; want at least 2", len(mod.Resources))
}
got := strings.TrimSpace(state.String())
want := strings.TrimSpace(testTerraformApplyDependsCreateBeforeStr)
if got != want {
t.Fatalf("wrong final state\ngot:\n%s\n\nwant:\n%s", got, want)
}
// Test that things were managed _in the right order_
order := h.States
diffs := h.Diffs
if !order[0].IsNull() || diffs[0].Action == plans.Delete {
t.Fatalf("should create new instance first: %#v", order)
}
if order[1].GetAttr("id").AsString() != "baz" {
t.Fatalf("update must happen after create: %#v", order[1])
}
if order[2].GetAttr("id").AsString() != "bar" || diffs[2].Action != plans.Delete {
t.Fatalf("destroy must happen after update: %#v", order[2])
}
}
func TestContext2Apply_singleDestroy(t *testing.T) {
m := testModule(t, "apply-depends-create-before")
h := new(HookRecordApplyOrder)
p := testProvider("aws")
invokeCount := 0
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
invokeCount++
switch invokeCount {
case 1:
if req.PlannedState.IsNull() {
t.Fatalf("should not destroy")
}
if id := req.PlannedState.GetAttr("id"); id.IsKnown() {
t.Fatalf("should not have ID")
}
case 2:
if req.PlannedState.IsNull() {
t.Fatalf("should not destroy")
}
if id := req.PlannedState.GetAttr("id"); id.AsString() != "baz" {
t.Fatalf("should have id")
}
case 3:
if !req.PlannedState.IsNull() {
t.Fatalf("should destroy")
}
default:
t.Fatalf("bad invoke count %d", invokeCount)
}
return testApplyFn(req)
}
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "web",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","require_new":"ami-old"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "lb",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"baz","instance":"bar"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "web",
},
Module: addrs.RootModule,
},
},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
h.Active = true
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
if invokeCount != 3 {
t.Fatalf("bad: %d", invokeCount)
}
}
// GH-7824
func TestContext2Apply_issue7824(t *testing.T) {
p := testProvider("template")
p.PlanResourceChangeFn = testDiffFn
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"template_file": {
Attributes: map[string]*configschema.Attribute{
"template": {Type: cty.String, Optional: true},
"__template_requires_new": {Type: cty.Bool, Optional: true},
},
},
},
})
m, snap := testModuleWithSnapshot(t, "issue-7824")
// Apply cleanly step 0
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("template"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
// Write / Read plan to simulate running it through a Plan file
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers =
map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("template"): testProviderFuncFixed(p),
}
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
}
// This deals with the situation where a splat expression is used referring
// to another resource whose count is non-constant.
func TestContext2Apply_issue5254(t *testing.T) {
// Create a provider. We use "template" here just to match the repro
// we got from the issue itself.
p := testProvider("template")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"template_file": {
Attributes: map[string]*configschema.Attribute{
"template": {Type: cty.String, Optional: true},
"__template_requires_new": {Type: cty.Bool, Optional: true},
"id": {Type: cty.String, Computed: true},
"type": {Type: cty.String, Computed: true},
},
},
},
})
// Apply cleanly step 0
m := testModule(t, "issue-5254/step-0")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("template"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
m, snap := testModuleWithSnapshot(t, "issue-5254/step-1")
// Application success. Now make the modification and store a plan
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("template"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
// Write / Read plan to simulate running it through a Plan file
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers = map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("template"): testProviderFuncFixed(p),
}
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(`
template_file.child:
ID = foo
provider = provider["registry.terraform.io/hashicorp/template"]
__template_requires_new = true
template = Hi
type = template_file
Dependencies:
template_file.parent
template_file.parent.0:
ID = foo
provider = provider["registry.terraform.io/hashicorp/template"]
template = Hi
type = template_file
`)
if actual != expected {
t.Fatalf("wrong final state\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestContext2Apply_targetedWithTaintedInState(t *testing.T) {
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
m, snap := testModuleWithSnapshot(t, "apply-tainted-targets")
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.ifailedprovisioners").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"ifailedprovisioners"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "iambeingadded",
),
},
})
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
// Write / Read plan to simulate running it through a Plan file
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers = map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
}
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(`
aws_instance.iambeingadded:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.ifailedprovisioners: (tainted)
ID = ifailedprovisioners
provider = provider["registry.terraform.io/hashicorp/aws"]
`)
if actual != expected {
t.Fatalf("expected state: \n%s\ngot: \n%s", expected, actual)
}
}
// Higher level test exposing the bug this covers in
// TestResource_ignoreChangesRequired
func TestContext2Apply_ignoreChangesCreate(t *testing.T) {
m := testModule(t, "apply-ignore-changes-create")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
instanceSchema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
instanceSchema.Attributes["required_field"] = &configschema.Attribute{
Type: cty.String,
Required: true,
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
mod := state.RootModule()
if len(mod.Resources) != 1 {
t.Fatalf("bad: %s", state)
}
actual := strings.TrimSpace(state.String())
// Expect no changes from original state
expected := strings.TrimSpace(`
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
required_field = set
type = aws_instance
`)
if actual != expected {
t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual)
}
}
func TestContext2Apply_ignoreChangesWithDep(t *testing.T) {
m := testModule(t, "apply-ignore-changes-dep")
p := testProvider("aws")
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
resp.PlannedState = req.ProposedNewState
switch req.TypeName {
case "aws_instance":
resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: "ami"}})
case "aws_eip":
return testDiffFn(req)
default:
t.Fatalf("Unexpected type: %s", req.TypeName)
}
return
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123","ami":"ami-abcd1234"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo[1]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-bcd234","ami":"i-bcd234"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_eip.foo[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"eip-abc123","instance":"i-abc123"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
},
Module: addrs.RootModule,
},
},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_eip.foo[1]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"eip-bcd234","instance":"i-bcd234"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
},
Module: addrs.RootModule,
},
},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state.DeepCopy(), DefaultPlanOpts)
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
actual := strings.TrimSpace(s.String())
expected := strings.TrimSpace(state.String())
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
}
}
func TestContext2Apply_ignoreChangesAll(t *testing.T) {
m := testModule(t, "apply-ignore-changes-all")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
instanceSchema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
instanceSchema.Attributes["required_field"] = &configschema.Attribute{
Type: cty.String,
Required: true,
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("plan failed")
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
mod := state.RootModule()
if len(mod.Resources) != 1 {
t.Fatalf("bad: %s", state)
}
actual := strings.TrimSpace(state.String())
// Expect no changes from original state
expected := strings.TrimSpace(`
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
required_field = set
type = aws_instance
`)
if actual != expected {
t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual)
}
}
// https://github.com/hashicorp/terraform/issues/7378
func TestContext2Apply_destroyNestedModuleWithAttrsReferencingResource(t *testing.T) {
m, snap := testModuleWithSnapshot(t, "apply-destroy-nested-module-with-attrs")
p := testProvider("null")
p.PlanResourceChangeFn = testDiffFn
var state *states.State
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
// First plan and apply a create operation
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply err: %s", diags.Err())
}
}
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatalf("destroy plan err: %s", diags.Err())
}
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers = map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
}
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("destroy apply err: %s", diags.Err())
}
}
if !state.Empty() {
t.Fatalf("state after apply: %s\nwant empty state", spew.Sdump(state))
}
}
// If a data source explicitly depends on another resource, it's because we need
// that resource to be applied first.
func TestContext2Apply_dataDependsOn(t *testing.T) {
p := testProvider("null")
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "null_instance" "write" {
foo = "attribute"
}
data "null_data_source" "read" {
count = 1
depends_on = ["null_instance.write"]
}
resource "null_instance" "depends" {
foo = data.null_data_source.read[0].foo
}
`})
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
// the "provisioner" here writes to this variable, because the intent is to
// create a dependency which can't be viewed through the graph, and depends
// solely on the configuration providing "depends_on"
provisionerOutput := ""
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
// the side effect of the resource being applied
provisionerOutput = "APPLIED"
return testApplyFn(req)
}
p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("boop"),
"foo": cty.StringVal(provisionerOutput),
}),
}
}
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
root := state.Module(addrs.RootModuleInstance)
is := root.ResourceInstance(addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "null_data_source",
Name: "read",
}.Instance(addrs.IntKey(0)))
if is == nil {
t.Fatal("data resource instance is not present in state; should be")
}
var attrs map[string]interface{}
err := json.Unmarshal(is.Current.AttrsJSON, &attrs)
if err != nil {
t.Fatal(err)
}
actual := attrs["foo"]
expected := "APPLIED"
if actual != expected {
t.Fatalf("bad:\n%s", strings.TrimSpace(state.String()))
}
// run another plan to make sure the data source doesn't show as a change
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
for _, c := range plan.Changes.Resources {
if c.Action != plans.NoOp {
t.Fatalf("unexpected change for %s", c.Addr)
}
}
// now we cause a change in the first resource, which should trigger a plan
// in the data source, and the resource that depends on the data source
// must plan a change as well.
m = testModuleInline(t, map[string]string{
"main.tf": `
resource "null_instance" "write" {
foo = "new"
}
data "null_data_source" "read" {
depends_on = ["null_instance.write"]
}
resource "null_instance" "depends" {
foo = data.null_data_source.read.foo
}
`})
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
// the side effect of the resource being applied
provisionerOutput = "APPLIED_AGAIN"
return testApplyFn(req)
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
expectedChanges := map[string]plans.Action{
"null_instance.write": plans.Update,
"data.null_data_source.read": plans.Read,
"null_instance.depends": plans.Update,
}
for _, c := range plan.Changes.Resources {
if c.Action != expectedChanges[c.Addr.String()] {
t.Errorf("unexpected %s for %s", c.Action, c.Addr)
}
}
}
func TestContext2Apply_terraformWorkspace(t *testing.T) {
m := testModule(t, "apply-terraform-workspace")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Meta: &ContextMeta{Env: "foo"},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
actual := state.RootModule().OutputValues["output"]
expected := cty.StringVal("foo")
if actual == nil || actual.Value != expected {
t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected)
}
}
// verify that multiple config references only create a single depends_on entry
func TestContext2Apply_multiRef(t *testing.T) {
m := testModule(t, "apply-multi-ref")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
deps := state.Modules[""].Resources["aws_instance.other"].Instances[addrs.NoKey].Current.Dependencies
if len(deps) != 1 || deps[0].String() != "aws_instance.create" {
t.Fatalf("expected 1 depends_on entry for aws_instance.create, got %q", deps)
}
}
func TestContext2Apply_targetedModuleRecursive(t *testing.T) {
m := testModule(t, "apply-targeted-module-recursive")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Child("child", addrs.NoKey),
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
mod := state.Module(
addrs.RootModuleInstance.Child("child", addrs.NoKey).Child("subchild", addrs.NoKey),
)
if mod == nil {
t.Fatalf("no subchild module found in the state!\n\n%#v", state)
}
if len(mod.Resources) != 1 {
t.Fatalf("expected 1 resources, got: %#v", mod.Resources)
}
checkStateString(t, state, `
<no state>
module.child.subchild:
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
num = 2
type = aws_instance
`)
}
func TestContext2Apply_localVal(t *testing.T) {
m := testModule(t, "apply-local-val")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("error during apply: %s", diags.Err())
}
got := strings.TrimSpace(state.String())
want := strings.TrimSpace(`
<no state>
Outputs:
result_1 = hello
result_3 = hello world
`)
if got != want {
t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want)
}
}
func TestContext2Apply_destroyWithLocals(t *testing.T) {
m := testModule(t, "apply-destroy-with-locals")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.foo").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetOutputValue("name", cty.StringVal("test-bar"), false)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
s, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("error during apply: %s", diags.Err())
}
got := strings.TrimSpace(s.String())
want := strings.TrimSpace(`<no state>`)
if got != want {
t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want)
}
}
func TestContext2Apply_providerWithLocals(t *testing.T) {
m := testModule(t, "provider-with-locals")
p := testProvider("aws")
providerRegion := ""
// this should not be overridden during destroy
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
val := req.Config.GetAttr("region")
if !val.IsNull() {
providerRegion = val.AsString()
}
return
}
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
if state.HasManagedResourceInstanceObjects() {
t.Fatal("expected no state, got:", state)
}
if providerRegion != "bar" {
t.Fatalf("expected region %q, got: %q", "bar", providerRegion)
}
}
func TestContext2Apply_destroyWithProviders(t *testing.T) {
m := testModule(t, "destroy-module-with-provider")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
removed := state.EnsureModule(addrs.RootModuleInstance.Child("mod", addrs.NoKey).Child("removed", addrs.NoKey))
removed.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.child").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"].baz`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
// test that we can't destroy if the provider is missing
if _, diags := ctx.Plan(m, state, &PlanOpts{Mode: plans.DestroyMode}); diags == nil {
t.Fatal("expected plan error, provider.aws.baz doesn't exist")
}
// correct the state
state.Modules["module.mod.module.removed"].Resources["aws_instance.child"].ProviderConfig = mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"].bar`)
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("error during apply: %s", diags.Err())
}
got := strings.TrimSpace(state.String())
want := strings.TrimSpace("<no state>")
if got != want {
t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want)
}
}
func TestContext2Apply_providersFromState(t *testing.T) {
m := configs.NewEmptyConfig()
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
implicitProviderState := states.NewState()
impRoot := implicitProviderState.EnsureModule(addrs.RootModuleInstance)
impRoot.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
aliasedProviderState := states.NewState()
aliasRoot := aliasedProviderState.EnsureModule(addrs.RootModuleInstance)
aliasRoot.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"].bar`),
)
moduleProviderState := states.NewState()
moduleProviderRoot := moduleProviderState.EnsureModule(addrs.RootModuleInstance)
moduleProviderRoot.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`module.child.provider["registry.terraform.io/hashicorp/aws"]`),
)
for _, tc := range []struct {
name string
state *states.State
output string
err bool
}{
{
name: "add implicit provider",
state: implicitProviderState,
err: false,
output: "<no state>",
},
// an aliased provider must be in the config to remove a resource
{
name: "add aliased provider",
state: aliasedProviderState,
err: true,
},
// a provider in a module implies some sort of config, so this isn't
// allowed even without an alias
{
name: "add unaliased module provider",
state: moduleProviderState,
err: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, tc.state, DefaultPlanOpts)
if tc.err {
if diags == nil {
t.Fatal("expected error")
} else {
return
}
}
if !tc.err && diags.HasErrors() {
t.Fatal(diags.Err())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
checkStateString(t, state, "<no state>")
})
}
}
func TestContext2Apply_plannedInterpolatedCount(t *testing.T) {
m, snap := testModuleWithSnapshot(t, "apply-interpolated-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.test").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: Providers,
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("plan failed: %s", diags.Err())
}
// We'll marshal and unmarshal the plan here, to ensure that we have
// a clean new context as would be created if we separately ran
// terraform plan -out=tfplan && terraform apply tfplan
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers = Providers
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
// Applying the plan should now succeed
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply failed: %s", diags.Err())
}
}
func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) {
m, snap := testModuleWithSnapshot(t, "plan-destroy-interpolated-count")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a[1]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetOutputValue("out", cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("foo")}), false)
ctx := testContext2(t, &ContextOpts{
Providers: providers,
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatalf("plan failed: %s", diags.Err())
}
// We'll marshal and unmarshal the plan here, to ensure that we have
// a clean new context as would be created if we separately ran
// terraform plan -out=tfplan && terraform apply tfplan
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatalf("failed to round-trip through planfile: %s", err)
}
ctxOpts.Providers = providers
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("err: %s", diags.Err())
}
// Applying the plan should now succeed
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply failed: %s", diags.Err())
}
if !state.Empty() {
t.Fatalf("state not empty: %s\n", state)
}
}
func TestContext2Apply_scaleInMultivarRef(t *testing.T) {
m := testModule(t, "apply-resource-scale-in")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
}
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.one").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.two").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: Providers,
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"instance_count": {
Value: cty.NumberIntVal(0),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
{
addr := mustResourceInstanceAddr("aws_instance.one[0]")
change := plan.Changes.ResourceInstance(addr)
if change == nil {
t.Fatalf("no planned change for %s", addr)
}
// This test was originally written with Terraform v0.11 and earlier
// in mind, so it declares a no-key instance of aws_instance.one,
// but its configuration sets count (to zero) and so we end up first
// moving the no-key instance to the zero key and then planning to
// destroy the zero key.
if got, want := change.PrevRunAddr, mustResourceInstanceAddr("aws_instance.one"); !want.Equal(got) {
t.Errorf("wrong previous run address for %s %s; want %s", addr, got, want)
}
if got, want := change.Action, plans.Delete; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
if got, want := change.ActionReason, plans.ResourceInstanceDeleteBecauseCountIndex; got != want {
t.Errorf("wrong action reason for %s %s; want %s", addr, got, want)
}
}
{
addr := mustResourceInstanceAddr("aws_instance.two")
change := plan.Changes.ResourceInstance(addr)
if change == nil {
t.Fatalf("no planned change for %s", addr)
}
if got, want := change.PrevRunAddr, mustResourceInstanceAddr("aws_instance.two"); !want.Equal(got) {
t.Errorf("wrong previous run address for %s %s; want %s", addr, got, want)
}
if got, want := change.Action, plans.Update; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
if got, want := change.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
t.Errorf("wrong action reason for %s %s; want %s", addr, got, want)
}
}
// Applying the plan should now succeed
_, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
}
func TestContext2Apply_inconsistentWithPlan(t *testing.T) {
m := testModule(t, "apply-inconsistent-with-plan")
p := testProvider("test")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
})
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("before"),
}),
}
}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
return providers.ApplyResourceChangeResponse{
NewState: cty.ObjectVal(map[string]cty.Value{
// This is intentionally incorrect: because id was fixed at "before"
// during plan, it must not change during apply.
"id": cty.StringVal("after"),
}),
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatalf("apply succeeded; want error")
}
if got, want := diags.Err().Error(), "Provider produced inconsistent result after apply"; !strings.Contains(got, want) {
t.Fatalf("wrong error\ngot: %s\nshould contain: %s", got, want)
}
}
// Issue 19908 was about retaining an existing object in the state when an
// update to it fails and the provider does not return a partially-updated
// value for it. Previously we were incorrectly removing it from the state
// in that case, but instead it should be retained so the update can be
// retried.
func TestContext2Apply_issue19908(t *testing.T) {
m := testModule(t, "apply-issue19908")
p := testProvider("test")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test": {
Attributes: map[string]*configschema.Attribute{
"baz": {Type: cty.String, Required: true},
},
},
},
})
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
var diags tfdiags.Diagnostics
diags = diags.Append(fmt.Errorf("update failed"))
return providers.ApplyResourceChangeResponse{
Diagnostics: diags,
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"baz":"old"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatalf("apply succeeded; want error")
}
if got, want := diags.Err().Error(), "update failed"; !strings.Contains(got, want) {
t.Fatalf("wrong error\ngot: %s\nshould contain: %s", got, want)
}
mod := state.RootModule()
rs := mod.Resources["test.foo"]
if rs == nil {
t.Fatalf("test.foo not in state after apply, but should be")
}
is := rs.Instances[addrs.NoKey]
if is == nil {
t.Fatalf("test.foo not in state after apply, but should be")
}
obj := is.Current
if obj == nil {
t.Fatalf("test.foo has no current object in state after apply, but should do")
}
if got, want := obj.Status, states.ObjectReady; got != want {
t.Errorf("test.foo has wrong status %s after apply; want %s", got, want)
}
if got, want := obj.AttrsJSON, []byte(`"old"`); !bytes.Contains(got, want) {
t.Errorf("test.foo attributes JSON doesn't contain %s after apply\ngot: %s", want, got)
}
}
func TestContext2Apply_invalidIndexRef(t *testing.T) {
p := testProvider("test")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true, Computed: true},
},
},
},
})
p.PlanResourceChangeFn = testDiffFn
m := testModule(t, "apply-invalid-index")
c := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
diags := c.Validate(m)
if diags.HasErrors() {
t.Fatalf("unexpected validation failure: %s", diags.Err())
}
wantErr := `The given key does not identify an element in this collection value`
_, diags = c.Plan(m, states.NewState(), DefaultPlanOpts)
if !diags.HasErrors() {
t.Fatalf("plan succeeded; want error")
}
gotErr := diags.Err().Error()
if !strings.Contains(gotErr, wantErr) {
t.Fatalf("missing expected error\ngot: %s\n\nwant: error containing %q", gotErr, wantErr)
}
}
func TestContext2Apply_moduleReplaceCycle(t *testing.T) {
for _, mode := range []string{"normal", "cbd"} {
var m *configs.Config
switch mode {
case "normal":
m = testModule(t, "apply-module-replace-cycle")
case "cbd":
m = testModule(t, "apply-module-replace-cycle-cbd")
}
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
instanceSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"require_new": {Type: cty.String, Optional: true},
},
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"aws_instance": instanceSchema,
},
})
state := states.NewState()
modA := state.EnsureModule(addrs.RootModuleInstance.Child("a", addrs.NoKey))
modA.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "a",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a","require_new":"old"}`),
CreateBeforeDestroy: mode == "cbd",
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
modB := state.EnsureModule(addrs.RootModuleInstance.Child("b", addrs.NoKey))
modB.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "b",
}.Instance(addrs.IntKey(0)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"b","require_new":"old"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
)
aBefore, _ := plans.NewDynamicValue(
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("a"),
"require_new": cty.StringVal("old"),
}), instanceSchema.ImpliedType())
aAfter, _ := plans.NewDynamicValue(
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"require_new": cty.StringVal("new"),
}), instanceSchema.ImpliedType())
bBefore, _ := plans.NewDynamicValue(
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("b"),
"require_new": cty.StringVal("old"),
}), instanceSchema.ImpliedType())
bAfter, _ := plans.NewDynamicValue(
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"require_new": cty.UnknownVal(cty.String),
}), instanceSchema.ImpliedType())
var aAction plans.Action
switch mode {
case "normal":
aAction = plans.DeleteThenCreate
case "cbd":
aAction = plans.CreateThenDelete
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
changes := &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("a", addrs.NoKey)),
ProviderAddr: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: aAction,
Before: aBefore,
After: aAfter,
},
},
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "b",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance.Child("b", addrs.NoKey)),
ProviderAddr: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: plans.DeleteThenCreate,
Before: bBefore,
After: bAfter,
},
},
},
}
plan := &plans.Plan{
UIMode: plans.NormalMode,
Changes: changes,
PriorState: state.DeepCopy(),
PrevRunState: state.DeepCopy(),
}
t.Run(mode, func(t *testing.T) {
_, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
})
}
}
func TestContext2Apply_destroyDataCycle(t *testing.T) {
m, snap := testModuleWithSnapshot(t, "apply-destroy-data-cycle")
p := testProvider("null")
p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("new"),
"foo": cty.NullVal(cty.String),
}),
}
}
tp := testProvider("test")
tp.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "null_resource",
Name: "a",
}.Instance(addrs.IntKey(0)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("null"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.IntKey(0)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "null_data_source",
Name: "d",
},
Module: addrs.RootModule,
},
},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "null_data_source",
Name: "d",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"old"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("null"),
Module: addrs.RootModule,
},
)
Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("test"): testProviderFuncFixed(tp),
}
ctx := testContext2(t, &ContextOpts{
Providers: Providers,
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
diags.HasErrors()
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// We'll marshal and unmarshal the plan here, to ensure that we have
// a clean new context as would be created if we separately ran
// terraform plan -out=tfplan && terraform apply tfplan
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatal(err)
}
ctxOpts.Providers = Providers
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("failed to create context for plan: %s", diags.Err())
}
tp.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
foo := req.Config.GetAttr("foo")
if !foo.IsKnown() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown config value foo"))
return resp
}
if foo.AsString() != "new" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("wrong config value: %q", foo.AsString()))
}
return resp
}
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
}
func TestContext2Apply_taintedDestroyFailure(t *testing.T) {
m := testModule(t, "apply-destroy-tainted")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
// All destroys fail.
if req.PlannedState.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("failure"))
return
}
// c will also fail to create, meaning the existing tainted instance
// becomes deposed, ans is then promoted back to current.
// only C has a foo attribute
planned := req.PlannedState.AsValueMap()
foo, ok := planned["foo"]
if ok && !foo.IsNull() && foo.AsString() == "c" {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("failure"))
return
}
return testApplyFn(req)
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
})
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "a",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"a","foo":"a"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "b",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"b","foo":"b"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "c",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"id":"c","foo":"old"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
}
ctx := testContext2(t, &ContextOpts{
Providers: Providers,
Hooks: []Hook{&testHook{}},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
diags.HasErrors()
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
state, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("expected error")
}
root = state.Module(addrs.RootModuleInstance)
// the instance that failed to destroy should remain tainted
a := root.ResourceInstance(addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "a",
}.Instance(addrs.NoKey))
if a.Current.Status != states.ObjectTainted {
t.Fatal("test_instance.a should be tainted")
}
// b is create_before_destroy, and the destroy failed, so there should be 1
// deposed instance.
b := root.ResourceInstance(addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "b",
}.Instance(addrs.NoKey))
if b.Current.Status != states.ObjectReady {
t.Fatal("test_instance.b should be Ready")
}
if len(b.Deposed) != 1 {
t.Fatal("test_instance.b failed to keep deposed instance")
}
// the desposed c instance should be promoted back to Current, and remain
// tainted
c := root.ResourceInstance(addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "c",
}.Instance(addrs.NoKey))
if c.Current == nil {
t.Fatal("test_instance.c has no current instance, but it should")
}
if c.Current.Status != states.ObjectTainted {
t.Fatal("test_instance.c should be tainted")
}
if len(c.Deposed) != 0 {
t.Fatal("test_instance.c should have no deposed instances")
}
if string(c.Current.AttrsJSON) != `{"foo":"old","id":"c"}` {
t.Fatalf("unexpected attrs for c: %q\n", c.Current.AttrsJSON)
}
}
func TestContext2Apply_plannedConnectionRefs(t *testing.T) {
m := testModule(t, "apply-plan-connection-refs")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
s := req.PlannedState.AsValueMap()
// delay "a" slightly, so if the reference edge is missing the "b"
// provisioner will see an unknown value.
if s["foo"].AsString() == "a" {
time.Sleep(500 * time.Millisecond)
}
s["id"] = cty.StringVal("ID")
if ty, ok := s["type"]; ok && !ty.IsKnown() {
s["type"] = cty.StringVal(req.TypeName)
}
resp.NewState = cty.ObjectVal(s)
return resp
}
provisionerFactory := func() (provisioners.Interface, error) {
pr := testProvisioner()
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
host := req.Connection.GetAttr("host")
if host.IsNull() || !host.IsKnown() {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("invalid host value: %#v", host))
}
return resp
}
return pr, nil
}
Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
}
provisioners := map[string]provisioners.Factory{
"shell": provisionerFactory,
}
hook := &testHook{}
ctx := testContext2(t, &ContextOpts{
Providers: Providers,
Provisioners: provisioners,
Hooks: []Hook{hook},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
diags.HasErrors()
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
}
func TestContext2Apply_cbdCycle(t *testing.T) {
m, snap := testModuleWithSnapshot(t, "apply-cbd-cycle")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "a",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a","require_new":"old","foo":"b"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "b",
},
Module: addrs.RootModule,
},
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "c",
},
Module: addrs.RootModule,
},
},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "b",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"b","require_new":"old","foo":"c"}`),
Dependencies: []addrs.ConfigResource{
{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "c",
},
Module: addrs.RootModule,
},
},
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "c",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"c","require_new":"old"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
Providers := map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
}
hook := &testHook{}
ctx := testContext2(t, &ContextOpts{
Providers: Providers,
Hooks: []Hook{hook},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
diags.HasErrors()
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
// We'll marshal and unmarshal the plan here, to ensure that we have
// a clean new context as would be created if we separately ran
// terraform plan -out=tfplan && terraform apply tfplan
ctxOpts, m, plan, err := contextOptsForPlanViaFile(snap, plan)
if err != nil {
t.Fatal(err)
}
ctxOpts.Providers = Providers
ctx, diags = NewContext(ctxOpts)
if diags.HasErrors() {
t.Fatalf("failed to create context for plan: %s", diags.Err())
}
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
}
func TestContext2Apply_ProviderMeta_apply_set(t *testing.T) {
m := testModule(t, "provider-meta-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
var pmMu sync.Mutex
arcPMs := map[string]cty.Value{}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
pmMu.Lock()
defer pmMu.Unlock()
arcPMs[req.TypeName] = req.ProviderMeta
s := req.PlannedState.AsValueMap()
s["id"] = cty.StringVal("ID")
if ty, ok := s["type"]; ok && !ty.IsKnown() {
s["type"] = cty.StringVal(req.TypeName)
}
return providers.ApplyResourceChangeResponse{
NewState: cty.ObjectVal(s),
}
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
if !p.ApplyResourceChangeCalled {
t.Fatalf("ApplyResourceChange not called")
}
expectations := map[string]cty.Value{}
if pm, ok := arcPMs["test_resource"]; !ok {
t.Fatalf("sub-module ApplyResourceChange not called")
} else if pm.IsNull() {
t.Fatalf("null ProviderMeta in sub-module ApplyResourceChange")
} else {
expectations["quux-submodule"] = pm
}
if pm, ok := arcPMs["test_instance"]; !ok {
t.Fatalf("root module ApplyResourceChange not called")
} else if pm.IsNull() {
t.Fatalf("null ProviderMeta in root module ApplyResourceChange")
} else {
expectations["quux"] = pm
}
type metaStruct struct {
Baz string `cty:"baz"`
}
for expected, v := range expectations {
var meta metaStruct
err := gocty.FromCtyValue(v, &meta)
if err != nil {
t.Fatalf("Error parsing cty value: %s", err)
}
if meta.Baz != expected {
t.Fatalf("Expected meta.Baz to be %q, got %q", expected, meta.Baz)
}
}
}
func TestContext2Apply_ProviderMeta_apply_unset(t *testing.T) {
m := testModule(t, "provider-meta-unset")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
var pmMu sync.Mutex
arcPMs := map[string]cty.Value{}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
pmMu.Lock()
defer pmMu.Unlock()
arcPMs[req.TypeName] = req.ProviderMeta
s := req.PlannedState.AsValueMap()
s["id"] = cty.StringVal("ID")
if ty, ok := s["type"]; ok && !ty.IsKnown() {
s["type"] = cty.StringVal(req.TypeName)
}
return providers.ApplyResourceChangeResponse{
NewState: cty.ObjectVal(s),
}
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
if !p.ApplyResourceChangeCalled {
t.Fatalf("ApplyResourceChange not called")
}
if pm, ok := arcPMs["test_resource"]; !ok {
t.Fatalf("sub-module ApplyResourceChange not called")
} else if !pm.IsNull() {
t.Fatalf("non-null ProviderMeta in sub-module ApplyResourceChange: %+v", pm)
}
if pm, ok := arcPMs["test_instance"]; !ok {
t.Fatalf("root module ApplyResourceChange not called")
} else if !pm.IsNull() {
t.Fatalf("non-null ProviderMeta in root module ApplyResourceChange: %+v", pm)
}
}
func TestContext2Apply_ProviderMeta_plan_set(t *testing.T) {
m := testModule(t, "provider-meta-set")
p := testProvider("test")
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
prcPMs := map[string]cty.Value{}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
prcPMs[req.TypeName] = req.ProviderMeta
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
if !p.PlanResourceChangeCalled {
t.Fatalf("PlanResourceChange not called")
}
expectations := map[string]cty.Value{}
if pm, ok := prcPMs["test_resource"]; !ok {
t.Fatalf("sub-module PlanResourceChange not called")
} else if pm.IsNull() {
t.Fatalf("null ProviderMeta in sub-module PlanResourceChange")
} else {
expectations["quux-submodule"] = pm
}
if pm, ok := prcPMs["test_instance"]; !ok {
t.Fatalf("root module PlanResourceChange not called")
} else if pm.IsNull() {
t.Fatalf("null ProviderMeta in root module PlanResourceChange")
} else {
expectations["quux"] = pm
}
type metaStruct struct {
Baz string `cty:"baz"`
}
for expected, v := range expectations {
var meta metaStruct
err := gocty.FromCtyValue(v, &meta)
if err != nil {
t.Fatalf("Error parsing cty value: %s", err)
}
if meta.Baz != expected {
t.Fatalf("Expected meta.Baz to be %q, got %q", expected, meta.Baz)
}
}
}
func TestContext2Apply_ProviderMeta_plan_unset(t *testing.T) {
m := testModule(t, "provider-meta-unset")
p := testProvider("test")
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
prcPMs := map[string]cty.Value{}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
prcPMs[req.TypeName] = req.ProviderMeta
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
if !p.PlanResourceChangeCalled {
t.Fatalf("PlanResourceChange not called")
}
if pm, ok := prcPMs["test_resource"]; !ok {
t.Fatalf("sub-module PlanResourceChange not called")
} else if !pm.IsNull() {
t.Fatalf("non-null ProviderMeta in sub-module PlanResourceChange: %+v", pm)
}
if pm, ok := prcPMs["test_instance"]; !ok {
t.Fatalf("root module PlanResourceChange not called")
} else if !pm.IsNull() {
t.Fatalf("non-null ProviderMeta in root module PlanResourceChange: %+v", pm)
}
}
func TestContext2Apply_ProviderMeta_plan_setNoSchema(t *testing.T) {
m := testModule(t, "provider-meta-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if !diags.HasErrors() {
t.Fatalf("plan supposed to error, has no errors")
}
var rootErr, subErr bool
errorSummary := "The resource test_%s.bar belongs to a provider that doesn't support provider_meta blocks"
for _, diag := range diags {
if diag.Description().Summary != "Provider registry.terraform.io/hashicorp/test doesn't support provider_meta" {
t.Errorf("Unexpected error: %+v", diag.Description())
}
switch diag.Description().Detail {
case fmt.Sprintf(errorSummary, "instance"):
rootErr = true
case fmt.Sprintf(errorSummary, "resource"):
subErr = true
default:
t.Errorf("Unexpected error: %s", diag.Description())
}
}
if !rootErr {
t.Errorf("Expected unsupported provider_meta block error for root module, none received")
}
if !subErr {
t.Errorf("Expected unsupported provider_meta block error for sub-module, none received")
}
}
func TestContext2Apply_ProviderMeta_plan_setInvalid(t *testing.T) {
m := testModule(t, "provider-meta-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"quux": {
Type: cty.String,
Required: true,
},
},
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if !diags.HasErrors() {
t.Fatalf("plan supposed to error, has no errors")
}
var reqErr, invalidErr bool
for _, diag := range diags {
switch diag.Description().Summary {
case "Missing required argument":
if diag.Description().Detail == `The argument "quux" is required, but no definition was found.` {
reqErr = true
} else {
t.Errorf("Unexpected error %+v", diag.Description())
}
case "Unsupported argument":
if diag.Description().Detail == `An argument named "baz" is not expected here.` {
invalidErr = true
} else {
t.Errorf("Unexpected error %+v", diag.Description())
}
default:
t.Errorf("Unexpected error %+v", diag.Description())
}
}
if !reqErr {
t.Errorf("Expected missing required argument error, none received")
}
if !invalidErr {
t.Errorf("Expected unsupported argument error, none received")
}
}
func TestContext2Apply_ProviderMeta_refresh_set(t *testing.T) {
m := testModule(t, "provider-meta-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
rrcPMs := map[string]cty.Value{}
p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
rrcPMs[req.TypeName] = req.ProviderMeta
newState, err := p.GetProviderSchemaResponse.ResourceTypes[req.TypeName].Block.CoerceValue(req.PriorState)
if err != nil {
panic(err)
}
resp.NewState = newState
return resp
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
_, diags = ctx.Refresh(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
if !p.ReadResourceCalled {
t.Fatalf("ReadResource not called")
}
expectations := map[string]cty.Value{}
if pm, ok := rrcPMs["test_resource"]; !ok {
t.Fatalf("sub-module ReadResource not called")
} else if pm.IsNull() {
t.Fatalf("null ProviderMeta in sub-module ReadResource")
} else {
expectations["quux-submodule"] = pm
}
if pm, ok := rrcPMs["test_instance"]; !ok {
t.Fatalf("root module ReadResource not called")
} else if pm.IsNull() {
t.Fatalf("null ProviderMeta in root module ReadResource")
} else {
expectations["quux"] = pm
}
type metaStruct struct {
Baz string `cty:"baz"`
}
for expected, v := range expectations {
var meta metaStruct
err := gocty.FromCtyValue(v, &meta)
if err != nil {
t.Fatalf("Error parsing cty value: %s", err)
}
if meta.Baz != expected {
t.Fatalf("Expected meta.Baz to be %q, got %q", expected, meta.Baz)
}
}
}
func TestContext2Apply_ProviderMeta_refresh_setNoSchema(t *testing.T) {
m := testModule(t, "provider-meta-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
// we need a schema for plan/apply so they don't error
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
// drop the schema before refresh, to test that it errors
schema.ProviderMeta = nil
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags = ctx.Refresh(m, state, DefaultPlanOpts)
if !diags.HasErrors() {
t.Fatalf("refresh supposed to error, has no errors")
}
var rootErr, subErr bool
errorSummary := "The resource test_%s.bar belongs to a provider that doesn't support provider_meta blocks"
for _, diag := range diags {
if diag.Description().Summary != "Provider registry.terraform.io/hashicorp/test doesn't support provider_meta" {
t.Errorf("Unexpected error: %+v", diag.Description())
}
switch diag.Description().Detail {
case fmt.Sprintf(errorSummary, "instance"):
rootErr = true
case fmt.Sprintf(errorSummary, "resource"):
subErr = true
default:
t.Errorf("Unexpected error: %s", diag.Description())
}
}
if !rootErr {
t.Errorf("Expected unsupported provider_meta block error for root module, none received")
}
if !subErr {
t.Errorf("Expected unsupported provider_meta block error for sub-module, none received")
}
}
func TestContext2Apply_ProviderMeta_refresh_setInvalid(t *testing.T) {
m := testModule(t, "provider-meta-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
// we need a matching schema for plan/apply so they don't error
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
// change the schema before refresh, to test that it errors
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"quux": {
Type: cty.String,
Required: true,
},
},
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags = ctx.Refresh(m, state, DefaultPlanOpts)
if !diags.HasErrors() {
t.Fatalf("refresh supposed to error, has no errors")
}
var reqErr, invalidErr bool
for _, diag := range diags {
switch diag.Description().Summary {
case "Missing required argument":
if diag.Description().Detail == `The argument "quux" is required, but no definition was found.` {
reqErr = true
} else {
t.Errorf("Unexpected error %+v", diag.Description())
}
case "Unsupported argument":
if diag.Description().Detail == `An argument named "baz" is not expected here.` {
invalidErr = true
} else {
t.Errorf("Unexpected error %+v", diag.Description())
}
default:
t.Errorf("Unexpected error %+v", diag.Description())
}
}
if !reqErr {
t.Errorf("Expected missing required argument error, none received")
}
if !invalidErr {
t.Errorf("Expected unsupported argument error, none received")
}
}
func TestContext2Apply_ProviderMeta_refreshdata_set(t *testing.T) {
m := testModule(t, "provider-meta-data-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
rdsPMs := map[string]cty.Value{}
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
rdsPMs[req.TypeName] = req.ProviderMeta
switch req.TypeName {
case "test_data_source":
log.Printf("[TRACE] test_data_source RDSR returning")
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("yo"),
"foo": cty.StringVal("bar"),
}),
}
case "test_file":
log.Printf("[TRACE] test_file RDSR returning")
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("bar"),
"rendered": cty.StringVal("baz"),
"template": cty.StringVal(""),
}),
}
default:
// config drift, oops
log.Printf("[TRACE] unknown request TypeName: %q", req.TypeName)
return providers.ReadDataSourceResponse{}
}
}
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
_, diags = ctx.Refresh(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
if !p.ReadDataSourceCalled {
t.Fatalf("ReadDataSource not called")
}
expectations := map[string]cty.Value{}
if pm, ok := rdsPMs["test_file"]; !ok {
t.Fatalf("sub-module ReadDataSource not called")
} else if pm.IsNull() {
t.Fatalf("null ProviderMeta in sub-module ReadDataSource")
} else {
expectations["quux-submodule"] = pm
}
if pm, ok := rdsPMs["test_data_source"]; !ok {
t.Fatalf("root module ReadDataSource not called")
} else if pm.IsNull() {
t.Fatalf("null ProviderMeta in root module ReadDataSource")
} else {
expectations["quux"] = pm
}
type metaStruct struct {
Baz string `cty:"baz"`
}
for expected, v := range expectations {
var meta metaStruct
err := gocty.FromCtyValue(v, &meta)
if err != nil {
t.Fatalf("Error parsing cty value: %s", err)
}
if meta.Baz != expected {
t.Fatalf("Expected meta.Baz to be %q, got %q", expected, meta.Baz)
}
}
}
func TestContext2Apply_ProviderMeta_refreshdata_unset(t *testing.T) {
m := testModule(t, "provider-meta-data-unset")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {
Type: cty.String,
Required: true,
},
},
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
rdsPMs := map[string]cty.Value{}
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
rdsPMs[req.TypeName] = req.ProviderMeta
switch req.TypeName {
case "test_data_source":
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("yo"),
"foo": cty.StringVal("bar"),
}),
}
case "test_file":
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("bar"),
"rendered": cty.StringVal("baz"),
"template": cty.StringVal(""),
}),
}
default:
// config drift, oops
return providers.ReadDataSourceResponse{}
}
}
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
if !p.ReadDataSourceCalled {
t.Fatalf("ReadDataSource not called")
}
if pm, ok := rdsPMs["test_file"]; !ok {
t.Fatalf("sub-module ReadDataSource not called")
} else if !pm.IsNull() {
t.Fatalf("non-null ProviderMeta in sub-module ReadDataSource")
}
if pm, ok := rdsPMs["test_data_source"]; !ok {
t.Fatalf("root module ReadDataSource not called")
} else if !pm.IsNull() {
t.Fatalf("non-null ProviderMeta in root module ReadDataSource")
}
}
func TestContext2Apply_ProviderMeta_refreshdata_setNoSchema(t *testing.T) {
m := testModule(t, "provider-meta-data-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("yo"),
"foo": cty.StringVal("bar"),
}),
}
_, diags := ctx.Refresh(m, states.NewState(), DefaultPlanOpts)
if !diags.HasErrors() {
t.Fatalf("refresh supposed to error, has no errors")
}
var rootErr, subErr bool
errorSummary := "The resource data.test_%s.foo belongs to a provider that doesn't support provider_meta blocks"
for _, diag := range diags {
if diag.Description().Summary != "Provider registry.terraform.io/hashicorp/test doesn't support provider_meta" {
t.Errorf("Unexpected error: %+v", diag.Description())
}
switch diag.Description().Detail {
case fmt.Sprintf(errorSummary, "data_source"):
rootErr = true
case fmt.Sprintf(errorSummary, "file"):
subErr = true
default:
t.Errorf("Unexpected error: %s", diag.Description())
}
}
if !rootErr {
t.Errorf("Expected unsupported provider_meta block error for root module, none received")
}
if !subErr {
t.Errorf("Expected unsupported provider_meta block error for sub-module, none received")
}
}
func TestContext2Apply_ProviderMeta_refreshdata_setInvalid(t *testing.T) {
m := testModule(t, "provider-meta-data-set")
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
schema := p.ProviderSchema()
schema.ProviderMeta = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"quux": {
Type: cty.String,
Required: true,
},
},
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("yo"),
"foo": cty.StringVal("bar"),
}),
}
_, diags := ctx.Refresh(m, states.NewState(), DefaultPlanOpts)
if !diags.HasErrors() {
t.Fatalf("refresh supposed to error, has no errors")
}
var reqErr, invalidErr bool
for _, diag := range diags {
switch diag.Description().Summary {
case "Missing required argument":
if diag.Description().Detail == `The argument "quux" is required, but no definition was found.` {
reqErr = true
} else {
t.Errorf("Unexpected error %+v", diag.Description())
}
case "Unsupported argument":
if diag.Description().Detail == `An argument named "baz" is not expected here.` {
invalidErr = true
} else {
t.Errorf("Unexpected error %+v", diag.Description())
}
default:
t.Errorf("Unexpected error %+v", diag.Description())
}
}
if !reqErr {
t.Errorf("Expected missing required argument error, none received")
}
if !invalidErr {
t.Errorf("Expected unsupported argument error, none received")
}
}
func TestContext2Apply_expandModuleVariables(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
module "mod1" {
for_each = toset(["a"])
source = "./mod"
}
module "mod2" {
source = "./mod"
in = module.mod1["a"].out
}
`,
"mod/main.tf": `
resource "aws_instance" "foo" {
foo = var.in
}
variable "in" {
type = string
default = "default"
}
output "out" {
value = aws_instance.foo.id
}
`,
})
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
expected := `<no state>
module.mod1["a"]:
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = default
type = aws_instance
Outputs:
out = foo
module.mod2:
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = foo
type = aws_instance
Dependencies:
module.mod1.aws_instance.foo`
if state.String() != expected {
t.Fatalf("expected:\n%s\ngot:\n%s\n", expected, state)
}
}
func TestContext2Apply_inheritAndStoreCBD(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "aws_instance" "foo" {
}
resource "aws_instance" "cbd" {
foo = aws_instance.foo.id
lifecycle {
create_before_destroy = true
}
}
`,
})
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
foo := state.ResourceInstance(mustResourceInstanceAddr("aws_instance.foo"))
if !foo.Current.CreateBeforeDestroy {
t.Fatal("aws_instance.foo should also be create_before_destroy")
}
}
func TestContext2Apply_moduleDependsOn(t *testing.T) {
m := testModule(t, "apply-module-depends-on")
p := testProvider("test")
// each instance being applied should happen in sequential order
applied := int64(0)
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
cfg := req.Config.AsValueMap()
foo := cfg["foo"].AsString()
ord := atomic.LoadInt64(&applied)
resp := providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("data"),
"foo": cfg["foo"],
}),
}
if foo == "a" && ord < 4 {
// due to data source "a"'s module depending on instance 4, this
// should not be less than 4
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("data source a read too early"))
}
if foo == "b" && ord < 1 {
// due to data source "b"'s module depending on instance 1, this
// should not be less than 1
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("data source b read too early"))
}
return resp
}
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
state := req.PlannedState.AsValueMap()
num, _ := state["num"].AsBigFloat().Float64()
ord := int64(num)
if !atomic.CompareAndSwapInt64(&applied, ord-1, ord) {
actual := atomic.LoadInt64(&applied)
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("instance %d was applied after %d", ord, actual))
}
state["id"] = cty.StringVal(fmt.Sprintf("test_%d", ord))
state["type"] = cty.StringVal("test_instance")
resp.NewState = cty.ObjectVal(state)
return resp
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
for _, res := range plan.Changes.Resources {
if res.Action != plans.NoOp {
t.Fatalf("expected NoOp, got %s for %s", res.Action, res.Addr)
}
}
}
func TestContext2Apply_moduleSelfReference(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
module "test" {
source = "./test"
a = module.test.b
}
output "c" {
value = module.test.c
}
`,
"test/main.tf": `
variable "a" {}
resource "test_instance" "test" {
}
output "b" {
value = test_instance.test.id
}
output "c" {
value = var.a
}`})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
if !state.Empty() {
t.Fatal("expected empty state, got:", state)
}
}
func TestContext2Apply_moduleExpandDependsOn(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
module "child" {
count = 1
source = "./child"
depends_on = [test_instance.a, test_instance.b]
}
resource "test_instance" "a" {
}
resource "test_instance" "b" {
}
`,
"child/main.tf": `
resource "test_instance" "foo" {
}
output "myoutput" {
value = "literal string"
}
`})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
if !state.Empty() {
t.Fatal("expected empty state, got:", state)
}
}
func TestContext2Apply_scaleInCBD(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
variable "ct" {
type = number
}
resource "test_instance" "a" {
count = var.ct
}
resource "test_instance" "b" {
require_new = local.removable
lifecycle {
create_before_destroy = true
}
}
resource "test_instance" "c" {
require_new = test_instance.b.id
lifecycle {
create_before_destroy = true
}
}
output "out" {
value = join(".", test_instance.a[*].id)
}
locals {
removable = join(".", test_instance.a[*].id)
}
`})
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_instance.a[0]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a0"}`),
Dependencies: []addrs.ConfigResource{},
CreateBeforeDestroy: true,
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_instance.a[1]").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a1"}`),
Dependencies: []addrs.ConfigResource{},
CreateBeforeDestroy: true,
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_instance.b").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"b", "require_new":"old.old"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_instance.a")},
CreateBeforeDestroy: true,
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_instance.c").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"c", "require_new":"b"}`),
Dependencies: []addrs.ConfigResource{
mustConfigResourceAddr("test_instance.a"),
mustConfigResourceAddr("test_instance.b"),
},
CreateBeforeDestroy: true,
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
p := testProvider("test")
p.PlanResourceChangeFn = func(r providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
n := r.ProposedNewState.AsValueMap()
if r.PriorState.IsNull() {
n["id"] = cty.UnknownVal(cty.String)
resp.PlannedState = cty.ObjectVal(n)
return resp
}
p := r.PriorState.AsValueMap()
priorRN := p["require_new"]
newRN := n["require_new"]
if eq := priorRN.Equals(newRN); !eq.IsKnown() || eq.False() {
resp.RequiresReplace = []cty.Path{{cty.GetAttrStep{Name: "require_new"}}}
n["id"] = cty.UnknownVal(cty.String)
}
resp.PlannedState = cty.ObjectVal(n)
return resp
}
// reduce the count to 1
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"ct": &InputValue{
Value: cty.NumberIntVal(1),
SourceType: ValueFromCaller,
},
},
})
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
{
addr := mustResourceInstanceAddr("test_instance.a[0]")
change := plan.Changes.ResourceInstance(addr)
if change == nil {
t.Fatalf("no planned change for %s", addr)
}
if got, want := change.PrevRunAddr, mustResourceInstanceAddr("test_instance.a[0]"); !want.Equal(got) {
t.Errorf("wrong previous run address for %s %s; want %s", addr, got, want)
}
if got, want := change.Action, plans.NoOp; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
if got, want := change.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
t.Errorf("wrong action reason for %s %s; want %s", addr, got, want)
}
}
{
addr := mustResourceInstanceAddr("test_instance.a[1]")
change := plan.Changes.ResourceInstance(addr)
if change == nil {
t.Fatalf("no planned change for %s", addr)
}
if got, want := change.PrevRunAddr, mustResourceInstanceAddr("test_instance.a[1]"); !want.Equal(got) {
t.Errorf("wrong previous run address for %s %s; want %s", addr, got, want)
}
if got, want := change.Action, plans.Delete; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
if got, want := change.ActionReason, plans.ResourceInstanceDeleteBecauseCountIndex; got != want {
t.Errorf("wrong action reason for %s %s; want %s", addr, got, want)
}
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
log.Fatal(diags.ErrWithWarnings())
}
// check the output, as those can't cause an error planning the value
out := state.RootModule().OutputValues["out"].Value.AsString()
if out != "a0" {
t.Fatalf(`expected output "a0", got: %q`, out)
}
// reduce the count to 0
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"ct": &InputValue{
Value: cty.NumberIntVal(0),
SourceType: ValueFromCaller,
},
},
})
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
{
addr := mustResourceInstanceAddr("test_instance.a[0]")
change := plan.Changes.ResourceInstance(addr)
if change == nil {
t.Fatalf("no planned change for %s", addr)
}
if got, want := change.PrevRunAddr, mustResourceInstanceAddr("test_instance.a[0]"); !want.Equal(got) {
t.Errorf("wrong previous run address for %s %s; want %s", addr, got, want)
}
if got, want := change.Action, plans.Delete; got != want {
t.Errorf("wrong action for %s %s; want %s", addr, got, want)
}
if got, want := change.ActionReason, plans.ResourceInstanceDeleteBecauseCountIndex; got != want {
t.Errorf("wrong action reason for %s %s; want %s", addr, got, want)
}
}
{
addr := mustResourceInstanceAddr("test_instance.a[1]")
change := plan.Changes.ResourceInstance(addr)
if change != nil {
// It was already removed in the previous plan/apply
t.Errorf("unexpected planned change for %s", addr)
}
}
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
// check the output, as those can't cause an error planning the value
out = state.RootModule().OutputValues["out"].Value.AsString()
if out != "" {
t.Fatalf(`expected output "", got: %q`, out)
}
}
// Ensure that we can destroy when a provider references a resource that will
// also be destroyed
func TestContext2Apply_destroyProviderReference(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
provider "null" {
value = ""
}
module "mod" {
source = "./mod"
}
provider "test" {
value = module.mod.output
}
resource "test_instance" "bar" {
}
`,
"mod/main.tf": `
data "null_data_source" "foo" {
count = 1
}
output "output" {
value = data.null_data_source.foo[0].output
}
`})
schemaFn := func(name string) *ProviderSchema {
return &ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
},
},
ResourceTypes: map[string]*configschema.Block{
name + "_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
DataSources: map[string]*configschema.Block{
name + "_data_source": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"output": {
Type: cty.String,
Computed: true,
},
},
},
},
}
}
testP := new(MockProvider)
testP.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
return providers.ReadResourceResponse{NewState: req.PriorState}
}
testP.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schemaFn("test"))
providerConfig := ""
testP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
value := req.Config.GetAttr("value")
if value.IsKnown() && !value.IsNull() {
providerConfig = value.AsString()
} else {
providerConfig = ""
}
return resp
}
testP.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
if providerConfig != "valid" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provider config is %q", providerConfig))
return
}
return testApplyFn(req)
}
testP.PlanResourceChangeFn = testDiffFn
nullP := new(MockProvider)
nullP.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
return providers.ReadResourceResponse{NewState: req.PriorState}
}
nullP.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schemaFn("null"))
nullP.ApplyResourceChangeFn = testApplyFn
nullP.PlanResourceChangeFn = testDiffFn
nullP.ReadDataSourceResponse = &providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("ID"),
"output": cty.StringVal("valid"),
}),
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP),
addrs.NewDefaultProvider("null"): testProviderFuncFixed(nullP),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP),
addrs.NewDefaultProvider("null"): testProviderFuncFixed(nullP),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
t.Fatalf("destroy apply errors: %s", diags.Err())
}
}
// Destroying properly requires pruning out all unneeded config nodes to
// prevent incorrect expansion evaluation.
func TestContext2Apply_destroyInterModuleExpansion(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
data "test_data_source" "a" {
for_each = {
one = "thing"
}
}
locals {
module_input = {
for k, v in data.test_data_source.a : k => v.id
}
}
module "mod1" {
source = "./mod"
input = local.module_input
}
module "mod2" {
source = "./mod"
input = module.mod1.outputs
}
resource "test_instance" "bar" {
for_each = module.mod2.outputs
}
output "module_output" {
value = module.mod2.outputs
}
output "test_instances" {
value = test_instance.bar
}
`,
"mod/main.tf": `
variable "input" {
}
data "test_data_source" "foo" {
for_each = var.input
}
output "outputs" {
value = data.test_data_source.foo
}
`})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("data_source"),
"foo": cty.StringVal("output"),
}),
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
destroy := func() {
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("destroy apply errors: %s", diags.Err())
}
}
destroy()
// Destroying again from the empty state should not cause any errors either
destroy()
}
func TestContext2Apply_createBeforeDestroyWithModule(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
variable "v" {}
module "mod" {
source = "./mod"
in = var.v
}
resource "test_resource" "a" {
value = var.v
depends_on = [module.mod]
lifecycle {
create_before_destroy = true
}
}
`,
"mod/main.tf": `
variable "in" {}
resource "test_resource" "a" {
value = var.in
}
`})
p := testProvider("test")
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
proposed := req.ProposedNewState.AsValueMap()
proposed["id"] = cty.UnknownVal(cty.String)
return providers.PlanResourceChangeResponse{
PlannedState: cty.ObjectVal(proposed),
RequiresReplace: []cty.Path{{cty.GetAttrStep{Name: "value"}}},
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"v": &InputValue{
Value: cty.StringVal("A"),
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"v": &InputValue{
Value: cty.StringVal("B"),
},
},
})
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_forcedCBD(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
variable "v" {}
resource "test_instance" "a" {
require_new = var.v
}
resource "test_instance" "b" {
depends_on = [test_instance.a]
lifecycle {
create_before_destroy = true
}
}
`})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"v": &InputValue{
Value: cty.StringVal("A"),
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"v": &InputValue{
Value: cty.StringVal("B"),
},
},
})
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_removeReferencedResource(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
variable "ct" {
}
resource "test_resource" "to_remove" {
count = var.ct
}
resource "test_resource" "c" {
value = join("", test_resource.to_remove[*].id)
}
`})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"ct": &InputValue{
Value: cty.NumberIntVal(1),
},
},
})
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"ct": &InputValue{
Value: cty.NumberIntVal(0),
},
},
})
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_variableSensitivity(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
variable "sensitive_var" {
default = "foo"
sensitive = true
}
variable "sensitive_id" {
default = "secret id"
sensitive = true
}
resource "test_resource" "foo" {
value = var.sensitive_var
network_interface {
network_interface_id = var.sensitive_id
}
}`,
})
p := new(MockProvider)
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
return providers.ReadResourceResponse{NewState: req.PriorState}
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
Provider: &configschema.Block{},
ResourceTypes: map[string]*configschema.Block{
"test_resource": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"value": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"network_interface": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"network_interface_id": {Type: cty.String, Optional: true},
"device_index": {Type: cty.Number, Optional: true},
},
},
Nesting: configschema.NestingSet,
},
},
},
},
})
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
// Run a second apply with no changes
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
// Now change the variable value for sensitive_var
ctx = testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags = ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"sensitive_var": &InputValue{
Value: cty.StringVal("bar"),
},
},
})
assertNoErrors(t, diags)
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
}
func TestContext2Apply_variableSensitivityPropagation(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
variable "sensitive_map" {
type = map(string)
default = {
"x" = "foo"
}
sensitive = true
}
resource "test_resource" "foo" {
value = var.sensitive_map.x
}
`,
})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("plan errors: %s", diags.Err())
}
verifySensitiveValue := func(pvms []cty.PathValueMarks) {
if len(pvms) != 1 {
t.Fatalf("expected 1 sensitive path, got %d", len(pvms))
}
pvm := pvms[0]
if gotPath, wantPath := pvm.Path, cty.GetAttrPath("value"); !gotPath.Equals(wantPath) {
t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath)
}
if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks)
}
}
addr := mustResourceInstanceAddr("test_resource.foo")
fooChangeSrc := plan.Changes.ResourceInstance(addr)
verifySensitiveValue(fooChangeSrc.AfterValMarks)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
fooState := state.ResourceInstance(addr)
verifySensitiveValue(fooState.Current.AttrSensitivePaths)
}
func TestContext2Apply_variableSensitivityProviders(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_resource" "foo" {
sensitive_value = "should get marked"
}
resource "test_resource" "bar" {
value = test_resource.foo.sensitive_value
random = test_resource.foo.id # not sensitive
nesting_single {
value = "abc"
sensitive_value = "xyz"
}
}
resource "test_resource" "baz" {
value = test_resource.bar.nesting_single.sensitive_value
}
`,
})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("plan errors: %s", diags.Err())
}
verifySensitiveValue := func(pvms []cty.PathValueMarks) {
if len(pvms) != 1 {
t.Fatalf("expected 1 sensitive path, got %d", len(pvms))
}
pvm := pvms[0]
if gotPath, wantPath := pvm.Path, cty.GetAttrPath("value"); !gotPath.Equals(wantPath) {
t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath)
}
if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks)
}
}
// Sensitive attributes (defined by the provider) are marked
// as sensitive when referenced from another resource
// "bar" references sensitive resources in "foo"
barAddr := mustResourceInstanceAddr("test_resource.bar")
barChangeSrc := plan.Changes.ResourceInstance(barAddr)
verifySensitiveValue(barChangeSrc.AfterValMarks)
bazAddr := mustResourceInstanceAddr("test_resource.baz")
bazChangeSrc := plan.Changes.ResourceInstance(bazAddr)
verifySensitiveValue(bazChangeSrc.AfterValMarks)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
barState := state.ResourceInstance(barAddr)
verifySensitiveValue(barState.Current.AttrSensitivePaths)
bazState := state.ResourceInstance(bazAddr)
verifySensitiveValue(bazState.Current.AttrSensitivePaths)
}
func TestContext2Apply_variableSensitivityChange(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
variable "sensitive_var" {
default = "hello"
sensitive = true
}
resource "test_resource" "foo" {
value = var.sensitive_var
}`,
})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo", "value":"hello"}`),
// No AttrSensitivePaths present
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
addr := mustResourceInstanceAddr("test_resource.foo")
state, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
fooState := state.ResourceInstance(addr)
if len(fooState.Current.AttrSensitivePaths) != 1 {
t.Fatalf("wrong number of sensitive paths, expected 1, got, %v", len(fooState.Current.AttrSensitivePaths))
}
got := fooState.Current.AttrSensitivePaths[0]
want := cty.PathValueMarks{
Path: cty.GetAttrPath("value"),
Marks: cty.NewValueMarks(marks.Sensitive),
}
if !got.Equal(want) {
t.Fatalf("wrong value marks; got:\n%#v\n\nwant:\n%#v\n", got, want)
}
m2 := testModuleInline(t, map[string]string{
"main.tf": `
variable "sensitive_var" {
default = "hello"
sensitive = false
}
resource "test_resource" "foo" {
value = var.sensitive_var
}`,
})
ctx2 := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
// NOTE: Prior to our refactoring to make the state an explicit argument
// of Plan, as opposed to hidden state inside Context, this test was
// calling ctx.Apply instead of ctx2.Apply and thus using the previous
// plan instead of this new plan. "Fixing" it to use the new plan seems
// to break the test, so we've preserved that oddity here by saving the
// old plan as oldPlan and essentially discarding the new plan entirely,
// but this seems rather suspicious and we should ideally figure out what
// this test was originally intending to do and make it do that.
oldPlan := plan
_, diags = ctx2.Plan(m2, state, DefaultPlanOpts)
assertNoErrors(t, diags)
stateWithoutSensitive, diags := ctx.Apply(oldPlan, m)
assertNoErrors(t, diags)
fooState2 := stateWithoutSensitive.ResourceInstance(addr)
if len(fooState2.Current.AttrSensitivePaths) > 0 {
t.Fatalf(
"wrong number of sensitive paths, expected 0, got, %v\n%s",
len(fooState2.Current.AttrSensitivePaths),
spew.Sdump(fooState2.Current.AttrSensitivePaths),
)
}
}
func TestContext2Apply_moduleVariableOptionalAttributes(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
terraform {
experiments = [module_variable_optional_attrs]
}
variable "in" {
type = object({
required = string
optional = optional(string)
})
}
output "out" {
value = var.in
}
`})
ctx := testContext2(t, &ContextOpts{})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"in": &InputValue{
Value: cty.MapVal(map[string]cty.Value{
"required": cty.StringVal("boop"),
}),
SourceType: ValueFromCaller,
},
},
})
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
got := state.RootModule().OutputValues["out"].Value
want := cty.ObjectVal(map[string]cty.Value{
"required": cty.StringVal("boop"),
// Because "optional" was marked as optional, it got silently filled
// in as a null value of string type rather than returning an error.
"optional": cty.NullVal(cty.String),
})
if !want.RawEquals(got) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
}
func TestContext2Apply_provisionerSensitive(t *testing.T) {
m := testModule(t, "apply-provisioner-sensitive")
p := testProvider("aws")
pr := testProvisioner()
pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
if req.Config.ContainsMarked() {
t.Fatalf("unexpectedly marked config value: %#v", req.Config)
}
command := req.Config.GetAttr("command")
if command.IsMarked() {
t.Fatalf("unexpectedly marked command argument: %#v", command.Marks())
}
req.UIOutput.Output(fmt.Sprintf("Executing: %q", command.AsString()))
return
}
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
h := new(MockHook)
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{h},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
Provisioners: map[string]provisioners.Factory{
"shell": testProvisionerFuncFixed(pr),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: InputValues{
"password": &InputValue{
Value: cty.StringVal("secret"),
SourceType: ValueFromCaller,
},
},
})
assertNoErrors(t, diags)
// "restart" provisioner
pr.CloseCalled = false
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
logDiagnostics(t, diags)
t.Fatal("apply failed")
}
actual := strings.TrimSpace(state.String())
expected := strings.TrimSpace(testTerraformApplyProvisionerSensitiveStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
// Verify apply was invoked
if !pr.ProvisionResourceCalled {
t.Fatalf("provisioner was not called on apply")
}
// Verify output was suppressed
if !h.ProvisionOutputCalled {
t.Fatalf("ProvisionOutput hook not called")
}
if got, doNotWant := h.ProvisionOutputMessage, "secret"; strings.Contains(got, doNotWant) {
t.Errorf("sensitive value %q included in output:\n%s", doNotWant, got)
}
if got, want := h.ProvisionOutputMessage, "output suppressed"; !strings.Contains(got, want) {
t.Errorf("expected hook to be called with %q, but was:\n%s", want, got)
}
}
func TestContext2Apply_warnings(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_resource" "foo" {
}`,
})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
resp := testApplyFn(req)
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.SimpleWarning("warning"))
return resp
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
inst := state.ResourceInstance(mustResourceInstanceAddr("test_resource.foo"))
if inst == nil {
t.Fatal("missing 'test_resource.foo' in state:", state)
}
}
func TestContext2Apply_rpcDiagnostics(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_instance" "a" {
}
`,
})
p := testProvider("test")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
resp = testApplyFn(req)
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.SimpleWarning("don't frobble"))
return resp
}
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
})
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
_, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
if len(diags) == 0 {
t.Fatal("expected warnings")
}
for _, d := range diags {
des := d.Description().Summary
if !strings.Contains(des, "frobble") {
t.Fatalf(`expected frobble, got %q`, des)
}
}
}
func TestContext2Apply_dataSensitive(t *testing.T) {
m := testModule(t, "apply-data-sensitive")
p := testProvider("null")
p.PlanResourceChangeFn = testDiffFn
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
// add the required id
m := req.Config.AsValueMap()
m["id"] = cty.StringVal("foo")
return providers.ReadDataSourceResponse{
State: cty.ObjectVal(m),
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
} else {
t.Logf(legacyDiffComparisonString(plan.Changes))
}
state, diags := ctx.Apply(plan, m)
assertNoErrors(t, diags)
addr := mustResourceInstanceAddr("data.null_data_source.testing")
dataSourceState := state.ResourceInstance(addr)
pvms := dataSourceState.Current.AttrSensitivePaths
if len(pvms) != 1 {
t.Fatalf("expected 1 sensitive path, got %d", len(pvms))
}
pvm := pvms[0]
if gotPath, wantPath := pvm.Path, cty.GetAttrPath("foo"); !gotPath.Equals(wantPath) {
t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath)
}
if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) {
t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks)
}
}
func TestContext2Apply_errorRestorePrivateData(t *testing.T) {
// empty config to remove our resource
m := testModuleInline(t, map[string]string{
"main.tf": "",
})
p := simpleMockProvider()
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{
// we error during apply, which will trigger core to preserve the last
// known state, including private data
Diagnostics: tfdiags.Diagnostics(nil).Append(errors.New("oops")),
}
addr := mustResourceInstanceAddr("test_object.a")
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
Private: []byte("private"),
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
})
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
state, _ = ctx.Apply(plan, m)
if string(state.ResourceInstance(addr).Current.Private) != "private" {
t.Fatal("missing private data in state")
}
}
func TestContext2Apply_errorRestoreStatus(t *testing.T) {
// empty config to remove our resource
m := testModuleInline(t, map[string]string{
"main.tf": "",
})
p := simpleMockProvider()
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
// We error during apply, but return the current object state.
resp.Diagnostics = resp.Diagnostics.Append(errors.New("oops"))
// return a warning too to make sure it isn't dropped
resp.Diagnostics = resp.Diagnostics.Append(tfdiags.SimpleWarning("warned"))
resp.NewState = req.PriorState
resp.Private = req.PlannedPrivate
return resp
}
addr := mustResourceInstanceAddr("test_object.a")
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"test_string":"foo"}`),
Private: []byte("private"),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.b")},
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
})
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
state, diags = ctx.Apply(plan, m)
errString := diags.ErrWithWarnings().Error()
if !strings.Contains(errString, "oops") || !strings.Contains(errString, "warned") {
t.Fatalf("error missing expected info: %q", errString)
}
if len(diags) != 2 {
t.Fatalf("expected 1 error and 1 warning, got: %q", errString)
}
res := state.ResourceInstance(addr)
if res == nil {
t.Fatal("resource was removed from state")
}
if res.Current.Status != states.ObjectTainted {
t.Fatal("resource should still be tainted in the state")
}
if len(res.Current.Dependencies) != 1 || !res.Current.Dependencies[0].Equal(mustConfigResourceAddr("test_object.b")) {
t.Fatalf("incorrect dependencies, got %q", res.Current.Dependencies)
}
if string(res.Current.Private) != "private" {
t.Fatalf("incorrect private data, got %q", res.Current.Private)
}
}
func TestContext2Apply_nonConformingResponse(t *testing.T) {
// empty config to remove our resource
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
test_string = "x"
}
`,
})
p := simpleMockProvider()
respDiags := tfdiags.Diagnostics(nil).Append(tfdiags.SimpleWarning("warned"))
respDiags = respDiags.Append(errors.New("oops"))
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{
// Don't lose these diagnostics
Diagnostics: respDiags,
// This state is missing required attributes, and should produce an error
NewState: cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("x"),
}),
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
_, diags = ctx.Apply(plan, m)
errString := diags.ErrWithWarnings().Error()
if !strings.Contains(errString, "oops") || !strings.Contains(errString, "warned") {
t.Fatalf("error missing expected info: %q", errString)
}
// we should have more than the ones returned from the provider, and they
// should not be coalesced into a single value
if len(diags) < 3 {
t.Fatalf("incorrect diagnostics, got %d values with %s", len(diags), diags.ErrWithWarnings())
}
}
func TestContext2Apply_nilResponse(t *testing.T) {
// empty config to remove our resource
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
}
`,
})
p := simpleMockProvider()
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
_, diags = ctx.Apply(plan, m)
if !diags.HasErrors() {
t.Fatal("expected and error")
}
errString := diags.ErrWithWarnings().Error()
if !strings.Contains(errString, "invalid nil value") {
t.Fatalf("error missing expected info: %q", errString)
}
}
////////////////////////////////////////////////////////////////////////////////
// NOTE: Due to the size of this file, new tests should be added to
// context_apply2_test.go.
////////////////////////////////////////////////////////////////////////////////