382 lines
9.6 KiB
Go
382 lines
9.6 KiB
Go
package local
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/backend"
|
|
"github.com/hashicorp/terraform/configs/configload"
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/plans/planfile"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestLocal_planBasic(t *testing.T) {
|
|
b, cleanup := TestLocal(t)
|
|
defer cleanup()
|
|
p := TestLocalProvider(t, b, "test", planFixtureSchema())
|
|
|
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
|
|
defer configCleanup()
|
|
op.PlanRefresh = true
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("plan operation failed")
|
|
}
|
|
|
|
if !p.PlanResourceChangeCalled {
|
|
t.Fatal("PlanResourceChange should be called")
|
|
}
|
|
}
|
|
|
|
func TestLocal_planInAutomation(t *testing.T) {
|
|
b, cleanup := TestLocal(t)
|
|
defer cleanup()
|
|
TestLocalProvider(t, b, "test", planFixtureSchema())
|
|
|
|
const msg = `You didn't specify an "-out" parameter`
|
|
|
|
// When we're "in automation" we omit certain text from the
|
|
// plan output. However, testing for the absense of text is
|
|
// unreliable in the face of future copy changes, so we'll
|
|
// mitigate that by running both with and without the flag
|
|
// set so we can ensure that the expected messages _are_
|
|
// included the first time.
|
|
b.RunningInAutomation = false
|
|
b.CLI = cli.NewMockUi()
|
|
{
|
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
|
|
defer configCleanup()
|
|
op.PlanRefresh = true
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("plan operation failed")
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, msg) {
|
|
t.Fatalf("missing next-steps message when not in automation")
|
|
}
|
|
}
|
|
|
|
// On the second run, we expect the next-steps messaging to be absent
|
|
// since we're now "running in automation".
|
|
b.RunningInAutomation = true
|
|
b.CLI = cli.NewMockUi()
|
|
{
|
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
|
|
defer configCleanup()
|
|
op.PlanRefresh = true
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("plan operation failed")
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if strings.Contains(output, msg) {
|
|
t.Fatalf("next-steps message present when in automation")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestLocal_planNoConfig(t *testing.T) {
|
|
b, cleanup := TestLocal(t)
|
|
defer cleanup()
|
|
TestLocalProvider(t, b, "test", &terraform.ProviderSchema{})
|
|
|
|
b.CLI = cli.NewMockUi()
|
|
|
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/empty")
|
|
defer configCleanup()
|
|
op.PlanRefresh = true
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("plan operation succeeded; want failure")
|
|
}
|
|
output := b.CLI.(*cli.MockUi).ErrorWriter.String()
|
|
if !strings.Contains(output, "configuration") {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestLocal_planRefreshFalse(t *testing.T) {
|
|
b, cleanup := TestLocal(t)
|
|
defer cleanup()
|
|
|
|
p := TestLocalProvider(t, b, "test", planFixtureSchema())
|
|
testStateFile(t, b.StatePath, testPlanState())
|
|
|
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
|
|
defer configCleanup()
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("plan operation failed")
|
|
}
|
|
|
|
if p.ReadResourceCalled {
|
|
t.Fatal("ReadResource should not be called")
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
t.Fatal("plan should be empty")
|
|
}
|
|
}
|
|
|
|
func TestLocal_planDestroy(t *testing.T) {
|
|
b, cleanup := TestLocal(t)
|
|
defer cleanup()
|
|
|
|
p := TestLocalProvider(t, b, "test", planFixtureSchema())
|
|
testStateFile(t, b.StatePath, testPlanState())
|
|
|
|
outDir := testTempDir(t)
|
|
defer os.RemoveAll(outDir)
|
|
planPath := filepath.Join(outDir, "plan.tfplan")
|
|
|
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
|
|
defer configCleanup()
|
|
op.Destroy = true
|
|
op.PlanRefresh = true
|
|
op.PlanOutPath = planPath
|
|
cfg := cty.ObjectVal(map[string]cty.Value{
|
|
"path": cty.StringVal(b.StatePath),
|
|
})
|
|
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
op.PlanOutBackend = &plans.Backend{
|
|
// Just a placeholder so that we can generate a valid plan file.
|
|
Type: "local",
|
|
Config: cfgRaw,
|
|
}
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("plan operation failed")
|
|
}
|
|
|
|
if !p.ReadResourceCalled {
|
|
t.Fatal("ReadResource should be called")
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
t.Fatal("plan should not be empty")
|
|
}
|
|
|
|
plan := testReadPlan(t, planPath)
|
|
for _, r := range plan.Changes.Resources {
|
|
if r.Action.String() != "Delete" {
|
|
t.Fatalf("bad: %#v", r.Action.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocal_planOutPathNoChange(t *testing.T) {
|
|
b, cleanup := TestLocal(t)
|
|
defer cleanup()
|
|
TestLocalProvider(t, b, "test", planFixtureSchema())
|
|
testStateFile(t, b.StatePath, testPlanState())
|
|
|
|
outDir := testTempDir(t)
|
|
defer os.RemoveAll(outDir)
|
|
planPath := filepath.Join(outDir, "plan.tfplan")
|
|
|
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan")
|
|
defer configCleanup()
|
|
op.PlanOutPath = planPath
|
|
cfg := cty.ObjectVal(map[string]cty.Value{
|
|
"path": cty.StringVal(b.StatePath),
|
|
})
|
|
cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
op.PlanOutBackend = &plans.Backend{
|
|
// Just a placeholder so that we can generate a valid plan file.
|
|
Type: "local",
|
|
Config: cfgRaw,
|
|
}
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("plan operation failed")
|
|
}
|
|
|
|
plan := testReadPlan(t, planPath)
|
|
|
|
if !plan.Changes.Empty() {
|
|
t.Fatalf("expected empty plan to be written")
|
|
}
|
|
}
|
|
|
|
// TestLocal_planScaleOutNoDupeCount tests a Refresh/Plan sequence when a
|
|
// resource count is scaled out. The scaled out node needs to exist in the
|
|
// graph and run through a plan-style sequence during the refresh phase, but
|
|
// can conflate the count if its post-diff count hooks are not skipped. This
|
|
// checks to make sure the correct resource count is ultimately given to the
|
|
// UI.
|
|
func TestLocal_planScaleOutNoDupeCount(t *testing.T) {
|
|
b, cleanup := TestLocal(t)
|
|
defer cleanup()
|
|
TestLocalProvider(t, b, "test", planFixtureSchema())
|
|
testStateFile(t, b.StatePath, testPlanState())
|
|
|
|
actual := new(CountHook)
|
|
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, actual)
|
|
|
|
outDir := testTempDir(t)
|
|
defer os.RemoveAll(outDir)
|
|
|
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-scaleout")
|
|
defer configCleanup()
|
|
op.PlanRefresh = true
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("plan operation failed")
|
|
}
|
|
|
|
expected := new(CountHook)
|
|
expected.ToAdd = 1
|
|
expected.ToChange = 0
|
|
expected.ToRemoveAndAdd = 0
|
|
expected.ToRemove = 0
|
|
|
|
if !reflect.DeepEqual(expected, actual) {
|
|
t.Fatalf("Expected %#v, got %#v instead.",
|
|
expected, actual)
|
|
}
|
|
}
|
|
|
|
func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) {
|
|
t.Helper()
|
|
|
|
_, configLoader, configCleanup := configload.MustLoadConfigForTests(t, configDir)
|
|
|
|
return &backend.Operation{
|
|
Type: backend.OperationTypePlan,
|
|
ConfigDir: configDir,
|
|
ConfigLoader: configLoader,
|
|
}, configCleanup
|
|
}
|
|
|
|
// testPlanState is just a common state that we use for testing plan.
|
|
func testPlanState() *states.State {
|
|
state := states.NewState()
|
|
rootModule := state.RootModule()
|
|
rootModule.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.IntKey(0)),
|
|
&states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
SchemaVersion: 1,
|
|
AttrsJSON: []byte(`{
|
|
"ami": "bar",
|
|
"network_interface": [{
|
|
"device_index": 0,
|
|
"description": "Main network interface"
|
|
}]
|
|
}`),
|
|
},
|
|
addrs.ProviderConfig{
|
|
Type: "test",
|
|
}.Absolute(addrs.RootModuleInstance),
|
|
)
|
|
return state
|
|
}
|
|
|
|
func testReadPlan(t *testing.T, path string) *plans.Plan {
|
|
t.Helper()
|
|
|
|
p, err := planfile.Open(path)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer p.Close()
|
|
|
|
plan, err := p.ReadPlan()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
return plan
|
|
}
|
|
|
|
// planFixtureSchema returns a schema suitable for processing the
|
|
// configuration in test-fixtures/plan . This schema should be
|
|
// assigned to a mock provider named "test".
|
|
func planFixtureSchema() *terraform.ProviderSchema {
|
|
return &terraform.ProviderSchema{
|
|
ResourceTypes: map[string]*configschema.Block{
|
|
"test_instance": {
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"ami": {Type: cty.String, Optional: true},
|
|
},
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"network_interface": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"device_index": {Type: cty.Number, Optional: true},
|
|
"description": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|