blob: dba659947919303f9982f2ce9875ffae6ae528bb [file] [log] [blame] [edit]
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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), td)
defer testChdir(t, td)()
statePath := testTempFile(t)
unlock, err := testLockState(t, 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), td)
defer testChdir(t, td)()
statePath := testTempFile(t)
unlock, err := testLockState(t, 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 := t.TempDir()
testCopyDir(t, testFixturePath("parallelism"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-config-invalid"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-error"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-input"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-input-partial"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := testCwd(t)
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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-shutdown"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-sensitive-output"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-vars"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-vars"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-vars"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-vars"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-terraform-env"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-terraform-env"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-targeted"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply-replace"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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 := t.TempDir()
testCopyDir(t, testFixturePath("apply"), 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" }
`