mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-26 08:51:02 -06:00
9c80574417
In order to test applying a plan from an existing state, we need to be able to inject the state meta into the planfile.
2341 lines
57 KiB
Go
2341 lines
57 KiB
Go
package command
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/command/views"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/states/statemgr"
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
tfversion "github.com/hashicorp/terraform/version"
|
|
)
|
|
|
|
func TestApply(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestApply_path(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
p := applyFixtureProvider()
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
testFixturePath("apply"),
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
if !strings.Contains(output.Stderr(), "-chdir") {
|
|
t.Fatal("expected command output to refer to -chdir flag, but got:", output.Stderr())
|
|
}
|
|
}
|
|
|
|
func TestApply_approveNo(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
defer testInputMap(t, map[string]string{
|
|
"approve": "no",
|
|
})()
|
|
|
|
// Do not use the NewMockUi initializer here, as we want to delay
|
|
// the call to init until after setting up the input mocks
|
|
ui := new(cli.MockUi)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
Ui: ui,
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
if got, want := output.Stdout(), "Apply cancelled"; !strings.Contains(got, want) {
|
|
t.Fatalf("expected output to include %q, but was:\n%s", want, got)
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err == nil || !os.IsNotExist(err) {
|
|
t.Fatalf("state file should not exist")
|
|
}
|
|
}
|
|
|
|
func TestApply_approveYes(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
|
|
defer testInputMap(t, map[string]string{
|
|
"approve": "yes",
|
|
})()
|
|
|
|
// Do not use the NewMockUi initializer here, as we want to delay
|
|
// the call to init until after setting up the input mocks
|
|
ui := new(cli.MockUi)
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
Ui: ui,
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
}
|
|
|
|
// test apply with locked state
|
|
func TestApply_lockedState(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
unlock, err := testLockState(testDataDir, statePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer unlock()
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code == 0 {
|
|
t.Fatal("expected error")
|
|
}
|
|
|
|
if !strings.Contains(output.Stderr(), "lock") {
|
|
t.Fatal("command output does not look like a lock error:", output.Stderr())
|
|
}
|
|
}
|
|
|
|
// test apply with locked state, waiting for unlock
|
|
func TestApply_lockedStateWait(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
unlock, err := testLockState(testDataDir, statePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// unlock during apply
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond)
|
|
unlock()
|
|
}()
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
// wait 4s just in case the lock process doesn't release in under a second,
|
|
// and we want our context to be alive for a second retry at the 3s mark.
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-lock-timeout", "4s",
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("lock should have succeeded in less than 3s: %s", output.Stderr())
|
|
}
|
|
}
|
|
|
|
// Verify that the parallelism flag allows no more than the desired number of
|
|
// concurrent calls to ApplyResourceChange.
|
|
func TestApply_parallelism(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("parallelism"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
par := 4
|
|
|
|
// started is a semaphore that we use to ensure that we never have more
|
|
// than "par" apply operations happening concurrently
|
|
started := make(chan struct{}, par)
|
|
|
|
// beginCtx is used as a starting gate to hold back ApplyResourceChange
|
|
// calls until we reach the desired concurrency. The cancel func "begin" is
|
|
// called once we reach the desired concurrency, allowing all apply calls
|
|
// to proceed in unison.
|
|
beginCtx, begin := context.WithCancel(context.Background())
|
|
|
|
// Since our mock provider has its own mutex preventing concurrent calls
|
|
// to ApplyResourceChange, we need to use a number of separate providers
|
|
// here. They will all have the same mock implementation function assigned
|
|
// but crucially they will each have their own mutex.
|
|
providerFactories := map[addrs.Provider]providers.Factory{}
|
|
for i := 0; i < 10; i++ {
|
|
name := fmt.Sprintf("test%d", i)
|
|
provider := &terraform.MockProvider{}
|
|
provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
name + "_instance": {Block: &configschema.Block{}},
|
|
},
|
|
}
|
|
provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
}
|
|
}
|
|
provider.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
|
|
// If we ever have more than our intended parallelism number of
|
|
// apply operations running concurrently, the semaphore will fail.
|
|
select {
|
|
case started <- struct{}{}:
|
|
defer func() {
|
|
<-started
|
|
}()
|
|
default:
|
|
t.Fatal("too many concurrent apply operations")
|
|
}
|
|
|
|
// If we never reach our intended parallelism, the context will
|
|
// never be canceled and the test will time out.
|
|
if len(started) >= par {
|
|
begin()
|
|
}
|
|
<-beginCtx.Done()
|
|
|
|
// do some "work"
|
|
// Not required for correctness, but makes it easier to spot a
|
|
// failure when there is more overlap.
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: cty.EmptyObjectVal,
|
|
}
|
|
}
|
|
providerFactories[addrs.NewDefaultProvider(name)] = providers.FactoryFixed(provider)
|
|
}
|
|
testingOverrides := &testingOverrides{
|
|
Providers: providerFactories,
|
|
}
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: testingOverrides,
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
fmt.Sprintf("-parallelism=%d", par),
|
|
}
|
|
|
|
res := c.Run(args)
|
|
output := done(t)
|
|
if res != 0 {
|
|
t.Fatal(output.Stdout())
|
|
}
|
|
}
|
|
|
|
func TestApply_configInvalid(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-config-invalid"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", testTempFile(t),
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("bad: \n%s", output.Stdout())
|
|
}
|
|
}
|
|
|
|
func TestApply_defaultState(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := filepath.Join(td, DefaultStateFilename)
|
|
|
|
// Change to the temporary directory
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := os.Chdir(filepath.Dir(statePath)); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer os.Chdir(cwd)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
// create an existing state file
|
|
localState := statemgr.NewFilesystem(statePath)
|
|
if err := localState.WriteState(states.NewState()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestApply_error(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-error"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
var lock sync.Mutex
|
|
errored := false
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
if !errored {
|
|
errored = true
|
|
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error"))
|
|
}
|
|
|
|
s := req.PlannedState.AsValueMap()
|
|
s["id"] = cty.StringVal("foo")
|
|
|
|
resp.NewState = cty.ObjectVal(s)
|
|
return
|
|
}
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
|
s := req.ProposedNewState.AsValueMap()
|
|
s["id"] = cty.UnknownVal(cty.String)
|
|
resp.PlannedState = cty.ObjectVal(s)
|
|
return
|
|
}
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"ami": {Type: cty.String, Optional: true},
|
|
"error": {Type: cty.Bool, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("wrong exit code %d; want 1\n%s", code, output.Stdout())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
if len(state.RootModule().Resources) == 0 {
|
|
t.Fatal("no resources in state")
|
|
}
|
|
}
|
|
|
|
func TestApply_input(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-input"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
// Disable test mode so input would be asked
|
|
test = false
|
|
defer func() { test = true }()
|
|
|
|
// The configuration for this test includes a declaration of variable
|
|
// "foo" with no default, and we don't set it on the command line below,
|
|
// so the apply command will produce an interactive prompt for the
|
|
// value of var.foo. We'll answer "foo" here, and we expect the output
|
|
// value "result" to echo that back to us below.
|
|
defaultInputReader = bytes.NewBufferString("foo\n")
|
|
defaultInputWriter = new(bytes.Buffer)
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
expected := strings.TrimSpace(`
|
|
<no state>
|
|
Outputs:
|
|
|
|
result = foo
|
|
`)
|
|
testStateOutput(t, statePath, expected)
|
|
}
|
|
|
|
// When only a partial set of the variables are set, Terraform
|
|
// should still ask for the unset ones by default (with -input=true)
|
|
func TestApply_inputPartial(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-input-partial"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
// Disable test mode so input would be asked
|
|
test = false
|
|
defer func() { test = true }()
|
|
|
|
// Set some default reader/writers for the inputs
|
|
defaultInputReader = bytes.NewBufferString("one\ntwo\n")
|
|
defaultInputWriter = new(bytes.Buffer)
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
"-var", "foo=foovalue",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
expected := strings.TrimSpace(`
|
|
<no state>
|
|
Outputs:
|
|
|
|
bar = one
|
|
foo = foovalue
|
|
`)
|
|
testStateOutput(t, statePath, expected)
|
|
}
|
|
|
|
func TestApply_noArgs(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestApply_plan(t *testing.T) {
|
|
// Disable test mode so input would be asked
|
|
test = false
|
|
defer func() { test = true }()
|
|
|
|
// Set some default reader/writers for the inputs
|
|
defaultInputReader = new(bytes.Buffer)
|
|
defaultInputWriter = new(bytes.Buffer)
|
|
|
|
planPath := applyFixturePlanFile(t)
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state-out", statePath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestApply_plan_backup(t *testing.T) {
|
|
statePath := testTempFile(t)
|
|
backupPath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
// create a state file that needs to be backed up
|
|
fs := statemgr.NewFilesystem(statePath)
|
|
fs.StateSnapshotMeta()
|
|
err := fs.WriteState(states.NewState())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// the plan file must contain the metadata from the prior state to be
|
|
// backed up
|
|
planPath := applyFixturePlanFileMatchState(t, fs.StateSnapshotMeta())
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-backup", backupPath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
// Should have a backup file
|
|
testStateRead(t, backupPath)
|
|
}
|
|
|
|
func TestApply_plan_noBackup(t *testing.T) {
|
|
planPath := applyFixturePlanFile(t)
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state-out", statePath,
|
|
"-backup", "-",
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
// Ensure there is no backup
|
|
_, err := os.Stat(statePath + DefaultBackupExtension)
|
|
if err == nil || !os.IsNotExist(err) {
|
|
t.Fatalf("backup should not exist")
|
|
}
|
|
|
|
// Ensure there is no literal "-"
|
|
_, err = os.Stat("-")
|
|
if err == nil || !os.IsNotExist(err) {
|
|
t.Fatalf("backup should not exist")
|
|
}
|
|
}
|
|
|
|
func TestApply_plan_remoteState(t *testing.T) {
|
|
// Disable test mode so input would be asked
|
|
test = false
|
|
defer func() { test = true }()
|
|
tmp, cwd := testCwd(t)
|
|
defer testFixCwd(t, tmp, cwd)
|
|
remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
|
if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Set some default reader/writers for the inputs
|
|
defaultInputReader = new(bytes.Buffer)
|
|
defaultInputWriter = new(bytes.Buffer)
|
|
|
|
// Create a remote state
|
|
state := testState()
|
|
_, srv := testRemoteState(t, state, 200)
|
|
defer srv.Close()
|
|
|
|
_, snap := testModuleWithSnapshot(t, "apply")
|
|
backendConfig := cty.ObjectVal(map[string]cty.Value{
|
|
"address": cty.StringVal(srv.URL),
|
|
"update_method": cty.NullVal(cty.String),
|
|
"lock_address": cty.NullVal(cty.String),
|
|
"unlock_address": cty.NullVal(cty.String),
|
|
"lock_method": cty.NullVal(cty.String),
|
|
"unlock_method": cty.NullVal(cty.String),
|
|
"username": cty.NullVal(cty.String),
|
|
"password": cty.NullVal(cty.String),
|
|
"skip_cert_verification": cty.NullVal(cty.Bool),
|
|
"retry_max": cty.NullVal(cty.String),
|
|
"retry_wait_min": cty.NullVal(cty.String),
|
|
"retry_wait_max": cty.NullVal(cty.String),
|
|
})
|
|
backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
planPath := testPlanFile(t, snap, state, &plans.Plan{
|
|
Backend: plans.Backend{
|
|
Type: "http",
|
|
Config: backendConfigRaw,
|
|
},
|
|
Changes: plans.NewChanges(),
|
|
})
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
// State file should be not be installed
|
|
if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil {
|
|
data, _ := ioutil.ReadFile(DefaultStateFilename)
|
|
t.Fatalf("State path should not exist: %s", string(data))
|
|
}
|
|
|
|
// Check that there is no remote state config
|
|
if src, err := ioutil.ReadFile(remoteStatePath); err == nil {
|
|
t.Fatalf("has %s file; should not\n%s", remoteStatePath, src)
|
|
}
|
|
}
|
|
|
|
func TestApply_planWithVarFile(t *testing.T) {
|
|
varFileDir := testTempDir(t)
|
|
varFilePath := filepath.Join(varFileDir, "terraform.tfvars")
|
|
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
planPath := applyFixturePlanFile(t)
|
|
statePath := testTempFile(t)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err := os.Chdir(varFileDir); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
defer os.Chdir(cwd)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state-out", statePath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestApply_planVars(t *testing.T) {
|
|
planPath := applyFixturePlanFile(t)
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-var", "foo=bar",
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code == 0 {
|
|
t.Fatal("should've failed: ", output.Stdout())
|
|
}
|
|
}
|
|
|
|
// we should be able to apply a plan file with no other file dependencies
|
|
func TestApply_planNoModuleFiles(t *testing.T) {
|
|
// temporary data directory which we can remove between commands
|
|
td := testTempDir(t)
|
|
defer os.RemoveAll(td)
|
|
|
|
defer testChdir(t, td)()
|
|
|
|
p := applyFixtureProvider()
|
|
planPath := applyFixturePlanFile(t)
|
|
view, done := testView(t)
|
|
apply := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
Ui: new(cli.MockUi),
|
|
View: view,
|
|
},
|
|
}
|
|
args := []string{
|
|
planPath,
|
|
}
|
|
apply.Run(args)
|
|
done(t)
|
|
}
|
|
|
|
func TestApply_refresh(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
originalState := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"ami":"bar"}`),
|
|
Status: states.ObjectReady,
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
})
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if !p.ReadResourceCalled {
|
|
t.Fatal("should call ReadResource")
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
|
|
// Should have a backup file
|
|
backupState := testStateRead(t, statePath+DefaultBackupExtension)
|
|
|
|
actualStr := strings.TrimSpace(backupState.String())
|
|
expectedStr := strings.TrimSpace(originalState.String())
|
|
if actualStr != expectedStr {
|
|
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
|
|
}
|
|
}
|
|
|
|
func TestApply_refreshFalse(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
originalState := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"ami":"bar"}`),
|
|
Status: states.ObjectReady,
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
})
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
"-refresh=false",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if p.ReadResourceCalled {
|
|
t.Fatal("should not call ReadResource when refresh=false")
|
|
}
|
|
}
|
|
func TestApply_shutdown(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-shutdown"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
cancelled := make(chan struct{})
|
|
shutdownCh := make(chan struct{})
|
|
|
|
statePath := testTempFile(t)
|
|
p := testProvider()
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
ShutdownCh: shutdownCh,
|
|
},
|
|
}
|
|
|
|
p.StopFn = func() error {
|
|
close(cancelled)
|
|
return nil
|
|
}
|
|
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
|
|
resp.PlannedState = req.ProposedNewState
|
|
return
|
|
}
|
|
|
|
var once sync.Once
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
|
|
// only cancel once
|
|
once.Do(func() {
|
|
shutdownCh <- struct{}{}
|
|
})
|
|
|
|
// Because of the internal lock in the MockProvider, we can't
|
|
// coordiante directly with the calling of Stop, and making the
|
|
// MockProvider concurrent is disruptive to a lot of existing tests.
|
|
// Wait here a moment to help make sure the main goroutine gets to the
|
|
// Stop call before we exit, or the plan may finish before it can be
|
|
// canceled.
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
resp.NewState = req.PlannedState
|
|
return
|
|
}
|
|
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"ami": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
select {
|
|
case <-cancelled:
|
|
default:
|
|
t.Fatal("command not cancelled")
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestApply_state(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
originalState := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"ami":"foo"}`),
|
|
Status: states.ObjectReady,
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
})
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
p := applyFixtureProvider()
|
|
p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{
|
|
PlannedState: cty.ObjectVal(map[string]cty.Value{
|
|
"ami": cty.StringVal("bar"),
|
|
}),
|
|
}
|
|
p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{
|
|
NewState: cty.ObjectVal(map[string]cty.Value{
|
|
"ami": cty.StringVal("bar"),
|
|
}),
|
|
}
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
// Run the apply command pointing to our existing state
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
// Verify that the provider was called with the existing state
|
|
actual := p.PlanResourceChangeRequest.PriorState
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.NullVal(cty.String),
|
|
"ami": cty.StringVal("foo"),
|
|
})
|
|
if !expected.RawEquals(actual) {
|
|
t.Fatalf("wrong prior state during plan\ngot: %#v\nwant: %#v", actual, expected)
|
|
}
|
|
|
|
actual = p.ApplyResourceChangeRequest.PriorState
|
|
expected = cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.NullVal(cty.String),
|
|
"ami": cty.StringVal("foo"),
|
|
})
|
|
if !expected.RawEquals(actual) {
|
|
t.Fatalf("wrong prior state during apply\ngot: %#v\nwant: %#v", actual, expected)
|
|
}
|
|
|
|
// Verify a new state exists
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
|
|
backupState := testStateRead(t, statePath+DefaultBackupExtension)
|
|
|
|
actualStr := strings.TrimSpace(backupState.String())
|
|
expectedStr := strings.TrimSpace(originalState.String())
|
|
if actualStr != expectedStr {
|
|
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
|
|
}
|
|
}
|
|
|
|
func TestApply_stateNoExist(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"idontexist.tfstate",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("bad: \n%s", output.Stdout())
|
|
}
|
|
}
|
|
|
|
func TestApply_sensitiveOutput(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-sensitive-output"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: \n%s", output.Stdout())
|
|
}
|
|
|
|
stdout := output.Stdout()
|
|
if !strings.Contains(stdout, "notsensitive = \"Hello world\"") {
|
|
t.Fatalf("bad: output should contain 'notsensitive' output\n%s", stdout)
|
|
}
|
|
if !strings.Contains(stdout, "sensitive = <sensitive>") {
|
|
t.Fatalf("bad: output should contain 'sensitive' output\n%s", stdout)
|
|
}
|
|
}
|
|
|
|
func TestApply_vars(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-vars"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
actual := ""
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"value": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: req.PlannedState,
|
|
}
|
|
}
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
actual = req.ProposedNewState.GetAttr("value").AsString()
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
}
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-var", "foo=bar",
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if actual != "bar" {
|
|
t.Fatal("didn't work")
|
|
}
|
|
}
|
|
|
|
func TestApply_varFile(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-vars"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
varFilePath := testTempFile(t)
|
|
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
actual := ""
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"value": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: req.PlannedState,
|
|
}
|
|
}
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
actual = req.ProposedNewState.GetAttr("value").AsString()
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
}
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-var-file", varFilePath,
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if actual != "bar" {
|
|
t.Fatal("didn't work")
|
|
}
|
|
}
|
|
|
|
func TestApply_varFileDefault(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-vars"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
varFilePath := filepath.Join(td, "terraform.tfvars")
|
|
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
actual := ""
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"value": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: req.PlannedState,
|
|
}
|
|
}
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
actual = req.ProposedNewState.GetAttr("value").AsString()
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
}
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if actual != "bar" {
|
|
t.Fatal("didn't work")
|
|
}
|
|
}
|
|
|
|
func TestApply_varFileDefaultJSON(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-vars"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
varFilePath := filepath.Join(td, "terraform.tfvars.json")
|
|
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFileJSON), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
actual := ""
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"value": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: req.PlannedState,
|
|
}
|
|
}
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
actual = req.ProposedNewState.GetAttr("value").AsString()
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
}
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if actual != "bar" {
|
|
t.Fatal("didn't work")
|
|
}
|
|
}
|
|
|
|
func TestApply_backup(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
originalState := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte("{\n \"id\": \"bar\"\n }"),
|
|
Status: states.ObjectReady,
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
})
|
|
statePath := testStateFile(t, originalState)
|
|
backupPath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{
|
|
PlannedState: cty.ObjectVal(map[string]cty.Value{
|
|
"ami": cty.StringVal("bar"),
|
|
}),
|
|
}
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
// Run the apply command pointing to our existing state
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-state", statePath,
|
|
"-backup", backupPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
// Verify a new state exists
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
|
|
backupState := testStateRead(t, backupPath)
|
|
|
|
actual := backupState.RootModule().Resources["test_instance.foo"]
|
|
expected := originalState.RootModule().Resources["test_instance.foo"]
|
|
if !cmp.Equal(actual, expected, cmpopts.EquateEmpty()) {
|
|
t.Fatalf(
|
|
"wrong aws_instance.foo state\n%s",
|
|
cmp.Diff(expected, actual, cmp.Transformer("bytesAsString", func(b []byte) string {
|
|
return string(b)
|
|
})),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestApply_disableBackup(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
originalState := testState()
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
p := applyFixtureProvider()
|
|
p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{
|
|
PlannedState: cty.ObjectVal(map[string]cty.Value{
|
|
"ami": cty.StringVal("bar"),
|
|
}),
|
|
}
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
// Run the apply command pointing to our existing state
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-state", statePath,
|
|
"-backup", "-",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
// Verify that the provider was called with the existing state
|
|
actual := p.PlanResourceChangeRequest.PriorState
|
|
expected := cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("bar"),
|
|
"ami": cty.NullVal(cty.String),
|
|
})
|
|
if !expected.RawEquals(actual) {
|
|
t.Fatalf("wrong prior state during plan\ngot: %#v\nwant: %#v", actual, expected)
|
|
}
|
|
|
|
actual = p.ApplyResourceChangeRequest.PriorState
|
|
expected = cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("bar"),
|
|
"ami": cty.NullVal(cty.String),
|
|
})
|
|
if !expected.RawEquals(actual) {
|
|
t.Fatalf("wrong prior state during apply\ngot: %#v\nwant: %#v", actual, expected)
|
|
}
|
|
|
|
// Verify a new state exists
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
|
|
// Ensure there is no backup
|
|
_, err := os.Stat(statePath + DefaultBackupExtension)
|
|
if err == nil || !os.IsNotExist(err) {
|
|
t.Fatalf("backup should not exist")
|
|
}
|
|
|
|
// Ensure there is no literal "-"
|
|
_, err = os.Stat("-")
|
|
if err == nil || !os.IsNotExist(err) {
|
|
t.Fatalf("backup should not exist")
|
|
}
|
|
}
|
|
|
|
// Test that the Terraform env is passed through
|
|
func TestApply_terraformEnv(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-terraform-env"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
expected := strings.TrimSpace(`
|
|
<no state>
|
|
Outputs:
|
|
|
|
output = default
|
|
`)
|
|
testStateOutput(t, statePath, expected)
|
|
}
|
|
|
|
// Test that the Terraform env is passed through
|
|
func TestApply_terraformEnvNonDefault(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-terraform-env"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
// Create new env
|
|
{
|
|
ui := new(cli.MockUi)
|
|
newCmd := &WorkspaceNewCommand{
|
|
Meta: Meta{
|
|
Ui: ui,
|
|
},
|
|
}
|
|
if code := newCmd.Run([]string{"test"}); code != 0 {
|
|
t.Fatal("error creating workspace")
|
|
}
|
|
}
|
|
|
|
// Switch to it
|
|
{
|
|
args := []string{"test"}
|
|
ui := new(cli.MockUi)
|
|
selCmd := &WorkspaceSelectCommand{
|
|
Meta: Meta{
|
|
Ui: ui,
|
|
},
|
|
}
|
|
if code := selCmd.Run(args); code != 0 {
|
|
t.Fatal("error switching workspace")
|
|
}
|
|
}
|
|
|
|
p := testProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
statePath := filepath.Join("terraform.tfstate.d", "test", "terraform.tfstate")
|
|
expected := strings.TrimSpace(`
|
|
<no state>
|
|
Outputs:
|
|
|
|
output = test
|
|
`)
|
|
testStateOutput(t, statePath, expected)
|
|
}
|
|
|
|
// Config with multiple resources, targeting apply of a subset
|
|
func TestApply_targeted(t *testing.T) {
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-targeted"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
p := testProvider()
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Computed: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
}
|
|
}
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-target", "test_instance.foo",
|
|
"-target", "test_instance.baz",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if got, want := output.Stdout(), "3 added, 0 changed, 0 destroyed"; !strings.Contains(got, want) {
|
|
t.Fatalf("bad change summary, want %q, got:\n%s", want, got)
|
|
}
|
|
}
|
|
|
|
// Diagnostics for invalid -target flags
|
|
func TestApply_targetFlagsDiags(t *testing.T) {
|
|
testCases := map[string]string{
|
|
"test_instance.": "Dot must be followed by attribute name.",
|
|
"test_instance": "Resource specification must include a resource type and name.",
|
|
}
|
|
|
|
for target, wantDiag := range testCases {
|
|
t.Run(target, func(t *testing.T) {
|
|
td := testTempDir(t)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-target", target,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
got := output.Stderr()
|
|
if !strings.Contains(got, target) {
|
|
t.Fatalf("bad error output, want %q, got:\n%s", target, got)
|
|
}
|
|
if !strings.Contains(got, wantDiag) {
|
|
t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApply_replace(t *testing.T) {
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply-replace"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
originalState := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "a",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"hello"}`),
|
|
Status: states.ObjectReady,
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
})
|
|
statePath := testStateFile(t, originalState)
|
|
|
|
p := testProvider()
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Computed: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
}
|
|
}
|
|
createCount := 0
|
|
deleteCount := 0
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
if req.PriorState.IsNull() {
|
|
createCount++
|
|
}
|
|
if req.PlannedState.IsNull() {
|
|
deleteCount++
|
|
}
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: req.PlannedState,
|
|
}
|
|
}
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-auto-approve",
|
|
"-state", statePath,
|
|
"-replace", "test_instance.a",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("wrong exit code %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if got, want := output.Stdout(), "1 added, 0 changed, 1 destroyed"; !strings.Contains(got, want) {
|
|
t.Errorf("wrong change summary\ngot output:\n%s\n\nwant substring: %s", got, want)
|
|
}
|
|
|
|
if got, want := createCount, 1; got != want {
|
|
t.Errorf("wrong create count %d; want %d", got, want)
|
|
}
|
|
if got, want := deleteCount, 1; got != want {
|
|
t.Errorf("wrong create count %d; want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestApply_pluginPath(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
pluginPath := []string{"a", "b", "c"}
|
|
|
|
if err := c.Meta.storePluginPath(pluginPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.Meta.pluginPath = nil
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if !reflect.DeepEqual(pluginPath, c.Meta.pluginPath) {
|
|
t.Fatalf("expected plugin path %#v, got %#v", pluginPath, c.Meta.pluginPath)
|
|
}
|
|
}
|
|
|
|
func TestApply_jsonGoldenReference(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-json",
|
|
"-state", statePath,
|
|
"-auto-approve",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
if _, err := os.Stat(statePath); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
state := testStateRead(t, statePath)
|
|
if state == nil {
|
|
t.Fatal("state should not be nil")
|
|
}
|
|
|
|
// Load the golden reference fixture
|
|
wantFile, err := os.Open(path.Join(testFixturePath("apply"), "output.jsonlog"))
|
|
if err != nil {
|
|
t.Fatalf("failed to open output file: %s", err)
|
|
}
|
|
defer wantFile.Close()
|
|
wantBytes, err := ioutil.ReadAll(wantFile)
|
|
if err != nil {
|
|
t.Fatalf("failed to read output file: %s", err)
|
|
}
|
|
want := string(wantBytes)
|
|
|
|
got := output.Stdout()
|
|
|
|
// Split the output and the reference into lines so that we can compare
|
|
// messages
|
|
got = strings.TrimSuffix(got, "\n")
|
|
gotLines := strings.Split(got, "\n")
|
|
|
|
want = strings.TrimSuffix(want, "\n")
|
|
wantLines := strings.Split(want, "\n")
|
|
|
|
if len(gotLines) != len(wantLines) {
|
|
t.Errorf("unexpected number of log lines: got %d, want %d", len(gotLines), len(wantLines))
|
|
}
|
|
|
|
// Verify that the log starts with a version message
|
|
type versionMessage struct {
|
|
Level string `json:"@level"`
|
|
Message string `json:"@message"`
|
|
Type string `json:"type"`
|
|
Terraform string `json:"terraform"`
|
|
UI string `json:"ui"`
|
|
}
|
|
var gotVersion versionMessage
|
|
if err := json.Unmarshal([]byte(gotLines[0]), &gotVersion); err != nil {
|
|
t.Errorf("failed to unmarshal version line: %s\n%s", err, gotLines[0])
|
|
}
|
|
wantVersion := versionMessage{
|
|
"info",
|
|
fmt.Sprintf("Terraform %s", tfversion.String()),
|
|
"version",
|
|
tfversion.String(),
|
|
views.JSON_UI_VERSION,
|
|
}
|
|
if !cmp.Equal(wantVersion, gotVersion) {
|
|
t.Errorf("unexpected first message:\n%s", cmp.Diff(wantVersion, gotVersion))
|
|
}
|
|
|
|
// Compare the rest of the lines against the golden reference
|
|
var gotLineMaps []map[string]interface{}
|
|
for i, line := range gotLines[1:] {
|
|
index := i + 1
|
|
var gotMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(line), &gotMap); err != nil {
|
|
t.Errorf("failed to unmarshal got line %d: %s\n%s", index, err, gotLines[index])
|
|
}
|
|
if _, ok := gotMap["@timestamp"]; !ok {
|
|
t.Errorf("missing @timestamp field in log: %s", gotLines[index])
|
|
}
|
|
delete(gotMap, "@timestamp")
|
|
gotLineMaps = append(gotLineMaps, gotMap)
|
|
}
|
|
var wantLineMaps []map[string]interface{}
|
|
for i, line := range wantLines[1:] {
|
|
index := i + 1
|
|
var wantMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(line), &wantMap); err != nil {
|
|
t.Errorf("failed to unmarshal want line %d: %s\n%s", index, err, gotLines[index])
|
|
}
|
|
wantLineMaps = append(wantLineMaps, wantMap)
|
|
}
|
|
if diff := cmp.Diff(wantLineMaps, gotLineMaps); diff != "" {
|
|
t.Errorf("wrong output lines\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestApply_warnings(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := tempDir(t)
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
defer os.RemoveAll(td)
|
|
defer testChdir(t, td)()
|
|
|
|
p := testProvider()
|
|
p.GetProviderSchemaResponse = applyFixtureSchema()
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
Diagnostics: tfdiags.Diagnostics{
|
|
tfdiags.SimpleWarning("warning 1"),
|
|
tfdiags.SimpleWarning("warning 2"),
|
|
},
|
|
}
|
|
}
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: cty.UnknownAsNull(req.PlannedState),
|
|
}
|
|
}
|
|
|
|
t.Run("full warnings", func(t *testing.T) {
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{"-auto-approve"}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
wantWarnings := []string{
|
|
"warning 1",
|
|
"warning 2",
|
|
}
|
|
for _, want := range wantWarnings {
|
|
if !strings.Contains(output.Stdout(), want) {
|
|
t.Errorf("missing warning %s", want)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("compact warnings", func(t *testing.T) {
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
code := c.Run([]string{"-auto-approve", "-compact-warnings"})
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
|
|
}
|
|
// the output should contain 2 warnings and a message about -compact-warnings
|
|
wantWarnings := []string{
|
|
"warning 1",
|
|
"warning 2",
|
|
"To see the full warning notes, run Terraform without -compact-warnings.",
|
|
}
|
|
for _, want := range wantWarnings {
|
|
if !strings.Contains(output.Stdout(), want) {
|
|
t.Errorf("missing warning %s", want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// applyFixtureSchema returns a schema suitable for processing the
|
|
// configuration in testdata/apply . This schema should be
|
|
// assigned to a mock provider named "test".
|
|
func applyFixtureSchema() *providers.GetProviderSchemaResponse {
|
|
return &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
"ami": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// applyFixtureProvider returns a mock provider that is configured for basic
|
|
// operation with the configuration in testdata/apply. This mock has
|
|
// GetSchemaResponse, PlanResourceChangeFn, and ApplyResourceChangeFn populated,
|
|
// with the plan/apply steps just passing through the data determined by
|
|
// Terraform Core.
|
|
func applyFixtureProvider() *terraform.MockProvider {
|
|
p := testProvider()
|
|
p.GetProviderSchemaResponse = applyFixtureSchema()
|
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
|
return providers.PlanResourceChangeResponse{
|
|
PlannedState: req.ProposedNewState,
|
|
}
|
|
}
|
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
|
return providers.ApplyResourceChangeResponse{
|
|
NewState: cty.UnknownAsNull(req.PlannedState),
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
// applyFixturePlanFile creates a plan file at a temporary location containing
|
|
// a single change to create the test_instance.foo that is included in the
|
|
// "apply" test fixture, returning the location of that plan file.
|
|
func applyFixturePlanFile(t *testing.T) string {
|
|
return applyFixturePlanFileMatchState(t, statemgr.SnapshotMeta{})
|
|
}
|
|
|
|
// applyFixturePlanFileMatchState creates a planfile like applyFixturePlanFile,
|
|
// but inserts the state meta information if that plan must match a preexisting
|
|
// state.
|
|
func applyFixturePlanFileMatchState(t *testing.T, stateMeta statemgr.SnapshotMeta) string {
|
|
_, snap := testModuleWithSnapshot(t, "apply")
|
|
plannedVal := cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.UnknownVal(cty.String),
|
|
"ami": cty.StringVal("bar"),
|
|
})
|
|
priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan := testPlan(t)
|
|
plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
|
|
Addr: addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
ProviderAddr: addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
Before: priorValRaw,
|
|
After: plannedValRaw,
|
|
},
|
|
})
|
|
return testPlanFileMatchState(
|
|
t,
|
|
snap,
|
|
states.NewState(),
|
|
plan,
|
|
stateMeta,
|
|
)
|
|
}
|
|
|
|
const applyVarFile = `
|
|
foo = "bar"
|
|
`
|
|
|
|
const applyVarFileJSON = `
|
|
{ "foo": "bar" }
|
|
`
|