mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3239 lines
82 KiB
3239 lines
82 KiB
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/hashicorp/cli"
|
|
version "github.com/hashicorp/go-version"
|
|
tfaddr "github.com/hashicorp/terraform-registry-address"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/collections"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/states/statemgr"
|
|
"github.com/hashicorp/terraform/internal/terminal"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
func TestApply(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := t.TempDir()
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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())
|
|
|
|
// This just makes go vet happy, in reality the function will never exit if
|
|
// begin() isn't called inside ApplyResourceChangeFn.
|
|
defer begin()
|
|
|
|
// 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 := &testing_provider.MockProvider{}
|
|
provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
name + "_instance": {Body: &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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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": {
|
|
Body: &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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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")
|
|
}
|
|
}
|
|
|
|
// Test the ability to apply a plan file with a state store.
|
|
//
|
|
// The state store's details (provider, config, etc) are supplied by the plan,
|
|
// which allows this test to not use any configuration.
|
|
func TestApply_plan_stateStore(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)
|
|
|
|
// Create the plan file that includes a state store
|
|
ver := version.Must(version.NewVersion("1.2.3"))
|
|
providerCfg := cty.ObjectVal(map[string]cty.Value{
|
|
"region": cty.StringVal("spain"),
|
|
})
|
|
providerCfgRaw, err := plans.NewDynamicValue(providerCfg, providerCfg.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storeCfg := cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("foobar"),
|
|
})
|
|
storeCfgRaw, err := plans.NewDynamicValue(storeCfg, storeCfg.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
plan := &plans.Plan{
|
|
Changes: plans.NewChangesSrc(),
|
|
|
|
// We'll default to the fake plan being both applyable and complete,
|
|
// since that's what most tests expect. Tests can override these
|
|
// back to false again afterwards if they need to.
|
|
Applyable: true,
|
|
Complete: true,
|
|
|
|
StateStore: &plans.StateStore{
|
|
Type: "test_store",
|
|
Provider: &plans.Provider{
|
|
Version: ver,
|
|
Source: &tfaddr.Provider{
|
|
Hostname: tfaddr.DefaultProviderRegistryHost,
|
|
Namespace: "hashicorp",
|
|
Type: "test",
|
|
},
|
|
Config: providerCfgRaw,
|
|
},
|
|
Config: storeCfgRaw,
|
|
Workspace: "default",
|
|
},
|
|
}
|
|
|
|
// Create a plan file on disk
|
|
//
|
|
// In this process we create a plan file describing the creation of a test_instance.foo resource.
|
|
state := testState() // State describes
|
|
_, snap := testModuleWithSnapshot(t, "apply")
|
|
planPath := testPlanFile(t, snap, state, plan)
|
|
|
|
// Create a mock, to be used as the pluggable state store described in the planfile
|
|
mock := testStateStoreMockWithChunkNegotiation(t, 1000)
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: &testingOverrides{
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock),
|
|
},
|
|
},
|
|
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())
|
|
}
|
|
|
|
if !mock.WriteStateBytesCalled {
|
|
t.Fatal("expected the test to write new state when applying the plan, but WriteStateBytesCalled is false on the mock provider.")
|
|
}
|
|
}
|
|
|
|
// Test unhappy paths when applying a plan file describing a state store.
|
|
func TestApply_plan_stateStore_errorCases(t *testing.T) {
|
|
// Disable test mode so input would be asked
|
|
test = false
|
|
defer func() { test = true }()
|
|
|
|
t.Run("error when the provider doesn't include the state store named in the plan", func(t *testing.T) {
|
|
// Set some default reader/writers for the inputs
|
|
defaultInputReader = new(bytes.Buffer)
|
|
defaultInputWriter = new(bytes.Buffer)
|
|
|
|
// Create the plan file that includes a state store
|
|
ver := version.Must(version.NewVersion("1.2.3"))
|
|
providerCfg := cty.ObjectVal(map[string]cty.Value{
|
|
"region": cty.StringVal("spain"),
|
|
})
|
|
providerCfgRaw, err := plans.NewDynamicValue(providerCfg, providerCfg.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storeCfg := cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("foobar"),
|
|
})
|
|
storeCfgRaw, err := plans.NewDynamicValue(storeCfg, storeCfg.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
plan := &plans.Plan{
|
|
Changes: plans.NewChangesSrc(),
|
|
|
|
// We'll default to the fake plan being both applyable and complete,
|
|
// since that's what most tests expect. Tests can override these
|
|
// back to false again afterwards if they need to.
|
|
Applyable: true,
|
|
Complete: true,
|
|
|
|
StateStore: &plans.StateStore{
|
|
Type: "test_doesnt_exist", // Mismatched with test_store in the provider
|
|
Provider: &plans.Provider{
|
|
Version: ver,
|
|
Source: &tfaddr.Provider{
|
|
Hostname: tfaddr.DefaultProviderRegistryHost,
|
|
Namespace: "hashicorp",
|
|
Type: "test",
|
|
},
|
|
Config: providerCfgRaw,
|
|
},
|
|
Config: storeCfgRaw,
|
|
Workspace: "default",
|
|
},
|
|
}
|
|
|
|
// Create a plan file on disk
|
|
//
|
|
// In this process we create a plan file describing the creation of a test_instance.foo resource.
|
|
state := testState() // State describes
|
|
_, snap := testModuleWithSnapshot(t, "apply")
|
|
planPath := testPlanFile(t, snap, state, plan)
|
|
|
|
// Create a mock, to be used as the pluggable state store described in the planfile
|
|
mock := testStateStoreMockWithChunkNegotiation(t, 1000)
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: &testingOverrides{
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock),
|
|
},
|
|
},
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
planPath,
|
|
"-no-color",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("expected an error but got none: %d\n\n%s", code, output.Stdout())
|
|
}
|
|
expectedErr := "Error: State store not implemented by the provider"
|
|
if !strings.Contains(output.Stderr(), expectedErr) {
|
|
t.Fatalf("expected error to include %q, but got:\n%s", expectedErr, output.Stderr())
|
|
}
|
|
})
|
|
|
|
t.Run("error when the provider doesn't implement state stores", func(t *testing.T) {
|
|
// Set some default reader/writers for the inputs
|
|
defaultInputReader = new(bytes.Buffer)
|
|
defaultInputWriter = new(bytes.Buffer)
|
|
|
|
// Create the plan file that includes a state store
|
|
ver := version.Must(version.NewVersion("1.2.3"))
|
|
providerCfg := cty.ObjectVal(map[string]cty.Value{
|
|
"region": cty.StringVal("spain"),
|
|
})
|
|
providerCfgRaw, err := plans.NewDynamicValue(providerCfg, providerCfg.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storeCfg := cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("foobar"),
|
|
})
|
|
storeCfgRaw, err := plans.NewDynamicValue(storeCfg, storeCfg.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
plan := &plans.Plan{
|
|
Changes: plans.NewChangesSrc(),
|
|
|
|
// We'll default to the fake plan being both applyable and complete,
|
|
// since that's what most tests expect. Tests can override these
|
|
// back to false again afterwards if they need to.
|
|
Applyable: true,
|
|
Complete: true,
|
|
|
|
StateStore: &plans.StateStore{
|
|
Type: "test_store",
|
|
Provider: &plans.Provider{
|
|
Version: ver,
|
|
Source: &tfaddr.Provider{
|
|
Hostname: tfaddr.DefaultProviderRegistryHost,
|
|
Namespace: "hashicorp",
|
|
Type: "test",
|
|
},
|
|
Config: providerCfgRaw,
|
|
},
|
|
Config: storeCfgRaw,
|
|
Workspace: "default",
|
|
},
|
|
}
|
|
|
|
// Create a plan file on disk
|
|
//
|
|
// In this process we create a plan file describing the creation of a test_instance.foo resource.
|
|
state := testState() // State describes
|
|
_, snap := testModuleWithSnapshot(t, "apply")
|
|
planPath := testPlanFile(t, snap, state, plan)
|
|
|
|
// Create a mock, to be used as the pluggable state store described in the planfile
|
|
mock := &testing_provider.MockProvider{}
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: &testingOverrides{
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock),
|
|
},
|
|
},
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
planPath,
|
|
"-no-color",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("expected an error but got none: %d\n\n%s", code, output.Stdout())
|
|
}
|
|
expectedErr := "Error: Provider does not support pluggable state storage"
|
|
if !strings.Contains(output.Stderr(), expectedErr) {
|
|
t.Fatalf("expected error to include %q, but got:\n%s", expectedErr, output.Stderr())
|
|
}
|
|
})
|
|
}
|
|
|
|
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 := t.TempDir()
|
|
t.Chdir(tmp)
|
|
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),
|
|
"client_ca_certificate_pem": cty.NullVal(cty.String),
|
|
"client_certificate_pem": cty.NullVal(cty.String),
|
|
"client_private_key_pem": 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,
|
|
Workspace: "default",
|
|
},
|
|
Changes: plans.NewChangesSrc(),
|
|
})
|
|
|
|
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 := os.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// The value of foo is the same as in the var file
|
|
planPath := applyFixturePlanFileWithVariableValue(t, "bar")
|
|
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()
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Computed: true},
|
|
"value": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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_planWithVarFileChangingVariableValue(t *testing.T) {
|
|
varFileDir := testTempDir(t)
|
|
varFilePath := filepath.Join(varFileDir, "terraform-test.tfvars")
|
|
if err := os.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// The value of foo is different from the var file
|
|
planPath := applyFixturePlanFileWithVariableValue(t, "lorem ipsum")
|
|
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,
|
|
"-var-file", varFilePath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code == 0 {
|
|
t.Fatalf("expected to fail, but succeeded. \n\n%s", output.All())
|
|
}
|
|
|
|
expectedTitle := "Can't change variable when applying a saved plan"
|
|
if !strings.Contains(output.Stderr(), expectedTitle) {
|
|
t.Fatalf("Expected stderr to contain %q, got %q", expectedTitle, output.Stderr())
|
|
}
|
|
}
|
|
|
|
func TestApply_planUndeclaredVars(t *testing.T) {
|
|
// This test ensures that it isn't allowed to set undeclared input variables
|
|
// when applying from a saved plan file, since in that case the variable
|
|
// values come from the saved plan file.
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
func TestApply_planWithEnvVars(t *testing.T) {
|
|
_, snap := testModuleWithSnapshot(t, "apply-output-only")
|
|
plan := testPlan(t)
|
|
|
|
addr, diags := addrs.ParseAbsOutputValueStr("output.shadow")
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
shadowVal := mustNewDynamicValue("noot", cty.DynamicPseudoType)
|
|
plan.VariableValues = map[string]plans.DynamicValue{
|
|
"shadow": shadowVal,
|
|
}
|
|
plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{
|
|
Addr: addr,
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
After: shadowVal,
|
|
},
|
|
})
|
|
planPath := testPlanFileMatchState(
|
|
t,
|
|
snap,
|
|
states.NewState(),
|
|
plan,
|
|
statemgr.SnapshotMeta{},
|
|
)
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
t.Setenv("TF_VAR_shadow", "env")
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-no-color",
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("unexpected failure: ", output.All())
|
|
}
|
|
|
|
expectedWarn := "Warning: Ignoring variable when applying a saved plan\n"
|
|
if !strings.Contains(output.Stdout(), expectedWarn) {
|
|
t.Fatalf("expected warning in output, given: %q", output.Stdout())
|
|
}
|
|
}
|
|
|
|
func TestApply_planWithSensitiveEnvVars(t *testing.T) {
|
|
_, snap := testModuleWithSnapshot(t, "apply-sensitive-variable")
|
|
plan := testPlan(t)
|
|
|
|
addr, diags := addrs.ParseAbsOutputValueStr("output.shadow")
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
shadowVal := mustNewDynamicValue("noot", cty.DynamicPseudoType)
|
|
plan.VariableValues = map[string]plans.DynamicValue{
|
|
"shadow": shadowVal,
|
|
}
|
|
plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{
|
|
Addr: addr,
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
After: shadowVal,
|
|
},
|
|
})
|
|
planPath := testPlanFileMatchState(
|
|
t,
|
|
snap,
|
|
states.NewState(),
|
|
plan,
|
|
statemgr.SnapshotMeta{},
|
|
)
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
t.Setenv("TF_VAR_shadow", "unique")
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-no-color",
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("unexpected failure: ", output.All())
|
|
}
|
|
|
|
out := output.Stdout()
|
|
|
|
expectedWarn := "Warning: Ignoring variable when applying a saved plan\n"
|
|
if !strings.Contains(out, expectedWarn) {
|
|
t.Fatalf("expected warning in output, given: %q", out)
|
|
}
|
|
|
|
if !strings.Contains(out, "(sensitive value)") {
|
|
t.Error("should have elided sensitive value")
|
|
}
|
|
|
|
if strings.Contains(out, "noot") {
|
|
t.Error("should have elided sensitive input, but contained value")
|
|
}
|
|
|
|
if strings.Contains(out, "unique") {
|
|
t.Error("should have elided sensitive input, but contained value")
|
|
}
|
|
}
|
|
|
|
// A saved plan includes a list of "apply-time variables", i.e. ephemeral
|
|
// input variables that were set during the plan, and must therefore be set
|
|
// during apply. No other variables may be set during apply.
|
|
//
|
|
// Test that an apply supplying all apply-time variables succeeds, and then test
|
|
// that supplying a declared ephemeral input variable that is *not* in the list
|
|
// of apply-time variables fails.
|
|
//
|
|
// In the fixture used for this test foo is a required ephemeral variable, whereas bar is
|
|
// an optional one.
|
|
func TestApply_planVarsEphemeral_applyTime(t *testing.T) {
|
|
for name, tc := range map[string]func(*testing.T, *ApplyCommand, string, string, func(*testing.T) *terminal.TestOutput){
|
|
"with planfile only passing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-var", "foo=bar",
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("should've succeeded: ", output.All())
|
|
}
|
|
},
|
|
|
|
"with planfile passing non-ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-var", "foo=bar",
|
|
"-var", "bar=bar",
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code == 0 {
|
|
t.Fatal("should've failed: ", output.All())
|
|
}
|
|
},
|
|
|
|
"with planfile missing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
args := []string{
|
|
"-state", statePath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code == 0 {
|
|
t.Fatal("should've failed: ", output.All())
|
|
}
|
|
},
|
|
|
|
"with planfile passing ephemeral variable through vars file": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
const planVarFile = `
|
|
foo = "bar"
|
|
`
|
|
|
|
// Write a tfvars file with the variable
|
|
tfVarsPath := testVarsFile(t)
|
|
err := os.WriteFile(tfVarsPath, []byte(planVarFile), 0600)
|
|
if err != nil {
|
|
t.Fatalf("Could not write vars file %e", err)
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-var-file", tfVarsPath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("should've succeeded: ", output.All())
|
|
}
|
|
},
|
|
|
|
"with planfile passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
t.Setenv("TF_VAR_foo", "bar")
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("should've succeeded: ", output.All())
|
|
}
|
|
},
|
|
|
|
"with planfile passing ephemeral variable through interactive prompts": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
close := testInteractiveInput(t, []string{"bar"})
|
|
defer close()
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code == 0 {
|
|
// We don't support interactive inputs for apply-time variables
|
|
t.Fatal("should have failed: ", output.All())
|
|
}
|
|
},
|
|
|
|
"without planfile only passing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-var", "foo=bar",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("should've succeeded: ", output.All())
|
|
}
|
|
},
|
|
|
|
"without planfile passing non-ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-var", "foo=bar",
|
|
"-var", "bar=bar",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
|
|
// For a combined plan & apply operation it's okay (and expected) to also be able to pass non-ephemeral variables
|
|
if code != 0 {
|
|
t.Fatal("should've succeeded: ", output.All())
|
|
}
|
|
},
|
|
|
|
"without planfile missing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
args := []string{
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code == 0 {
|
|
t.Fatal("should've failed: ", output.All())
|
|
}
|
|
},
|
|
|
|
"without planfile passing ephemeral variable through vars file": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
const planVarFile = `
|
|
foo = "bar"
|
|
`
|
|
|
|
// Write a tfvars file with the variable
|
|
tfVarsPath := testVarsFile(t)
|
|
err := os.WriteFile(tfVarsPath, []byte(planVarFile), 0600)
|
|
if err != nil {
|
|
t.Fatalf("Could not write vars file %e", err)
|
|
}
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
"-var-file", tfVarsPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("should've succeeded: ", output.All())
|
|
}
|
|
},
|
|
|
|
"without planfile passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
t.Setenv("TF_VAR_foo", "bar")
|
|
t.Setenv("TF_VAR_unused", `{key:"val"}`)
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("should've succeeded: ", output.All())
|
|
}
|
|
},
|
|
|
|
"without planfile passing ephemeral variable through interactive prompts": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
|
|
close := testInteractiveInput(t, []string{"bar"})
|
|
defer close()
|
|
|
|
args := []string{
|
|
"-state", statePath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatal("should've succeeded: ", output.All())
|
|
}
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
td := t.TempDir()
|
|
testCopyDir(t, testFixturePath("apply-ephemeral-variable"), td)
|
|
t.Chdir(td)
|
|
|
|
_, snap := testModuleWithSnapshot(t, "apply-ephemeral-variable")
|
|
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.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,
|
|
},
|
|
})
|
|
applyTimeVariables := collections.NewSetCmp[string]()
|
|
applyTimeVariables.Add("foo")
|
|
plan.ApplyTimeVariables = applyTimeVariables
|
|
|
|
planPath := testPlanFileMatchState(
|
|
t,
|
|
snap,
|
|
states.NewState(),
|
|
plan,
|
|
statemgr.SnapshotMeta{},
|
|
)
|
|
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
tc(t, c, statePath, planPath, done)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Variables can be passed to apply now for ephemeral usage, but we need to
|
|
// ensure that the legacy handling of undeclared variables remains intact
|
|
func TestApply_changedVars_applyTime(t *testing.T) {
|
|
t.Run("undeclared-config-var", func(t *testing.T) {
|
|
// an undeclared config variable is a warning, just like during plan
|
|
varFileDir := testTempDir(t)
|
|
varFilePath := filepath.Join(varFileDir, "terraform.tfvars")
|
|
if err := os.WriteFile(varFilePath, []byte(`undeclared = true`), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// The value of foo is not set
|
|
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("unexpected exit code %d:\n\n%s", code, output.All())
|
|
}
|
|
|
|
if !strings.Contains(output.All(), `Value for undeclared variable`) {
|
|
t.Fatalf("missing undeclared warning:\n%s", output.All())
|
|
}
|
|
})
|
|
|
|
t.Run("undeclared-cli-var", func(t *testing.T) {
|
|
// an undeclared cli variable is an error, just like during plan
|
|
planPath := applyFixturePlanFile(t)
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-var", "undeclared=true",
|
|
"-state-out", statePath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("unexpected exit code %d:\n\n%s", code, output.All())
|
|
}
|
|
|
|
if !strings.Contains(output.Stderr(), `Value for undeclared variable`) {
|
|
t.Fatalf("missing undeclared warning:\n%s", output.All())
|
|
}
|
|
})
|
|
|
|
t.Run("changed-cli-var", func(t *testing.T) {
|
|
planPath := applyFixturePlanFileWithVariableValue(t, "orig")
|
|
statePath := testTempFile(t)
|
|
|
|
p := applyFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-var", "foo=new",
|
|
"-state-out", statePath,
|
|
planPath,
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 1 {
|
|
t.Fatalf("unexpected exit code %d:\n\n%s", code, output.All())
|
|
}
|
|
|
|
if !strings.Contains(output.Stderr(), `Can't change variable when applying a saved plan`) {
|
|
t.Fatalf("missing undeclared warning:\n%s", output.All())
|
|
}
|
|
})
|
|
|
|
t.Run("var-file-override-auto", func(t *testing.T) {
|
|
// for this one we're going to do a full plan to make sure the variables
|
|
// can be applied consistently. The plan specifies a var file, and
|
|
// during apply we don't want to override that with the default or auto
|
|
// var files.
|
|
td := t.TempDir()
|
|
testCopyDir(t, testFixturePath("apply-vars-auto"), td)
|
|
t.Chdir(td)
|
|
|
|
p := planVarsFixtureProvider()
|
|
view, done := testView(t)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-var-file", "terraform-test.tfvars",
|
|
"-out", "planfile",
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
view, done = testView(t)
|
|
apply := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
Ui: new(cli.MockUi),
|
|
View: view,
|
|
},
|
|
}
|
|
args = []string{
|
|
"planfile",
|
|
}
|
|
code = apply.Run(args)
|
|
output = done(t)
|
|
if code != 0 {
|
|
t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr())
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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": {
|
|
Body: &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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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": {
|
|
Body: &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)
|
|
t.Chdir(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": {
|
|
Body: &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)
|
|
t.Chdir(td)
|
|
|
|
varFilePath := filepath.Join(td, "terraform.tfvars")
|
|
if err := os.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": {
|
|
Body: &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)
|
|
t.Chdir(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": {
|
|
Body: &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)
|
|
t.Chdir(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(), cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{})) {
|
|
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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(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)
|
|
t.Chdir(td)
|
|
|
|
p := testProvider()
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Body: &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)
|
|
t.Chdir(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)
|
|
t.Chdir(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": {
|
|
Body: &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)
|
|
t.Chdir(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)
|
|
t.Chdir(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")
|
|
}
|
|
|
|
checkGoldenReference(t, output, "apply")
|
|
}
|
|
|
|
func TestApply_warnings(t *testing.T) {
|
|
// Create a temporary working directory that is empty
|
|
td := t.TempDir()
|
|
testCopyDir(t, testFixturePath("apply"), td)
|
|
t.Chdir(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": {
|
|
Body: &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() *testing_provider.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.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,
|
|
)
|
|
}
|
|
|
|
// applyFixturePlanFileWithVariableValue creates a plan file at a temporary location containing
|
|
// a single change to create the test_instance.foo and a variable value that is included in the
|
|
// "apply" test fixture, returning the location of that plan file.
|
|
func applyFixturePlanFileWithVariableValue(t *testing.T, value string) string {
|
|
_, snap := testModuleWithSnapshot(t, "apply-vars")
|
|
plannedVal := cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.UnknownVal(cty.String),
|
|
"value": 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.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,
|
|
},
|
|
})
|
|
|
|
plan.VariableValues = map[string]plans.DynamicValue{
|
|
"foo": mustNewDynamicValue(value, cty.DynamicPseudoType),
|
|
}
|
|
return testPlanFileMatchState(
|
|
t,
|
|
snap,
|
|
states.NewState(),
|
|
plan,
|
|
statemgr.SnapshotMeta{},
|
|
)
|
|
}
|
|
|
|
const applyVarFile = `
|
|
foo = "bar"
|
|
`
|
|
|
|
const applyVarFileJSON = `
|
|
{ "foo": "bar" }
|
|
`
|
|
|
|
func mustNewDynamicValue(val string, ty cty.Type) plans.DynamicValue {
|
|
realVal := cty.StringVal(val)
|
|
ret, err := plans.NewDynamicValue(realVal, ty)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func TestProviderInconsistentFileFunc(t *testing.T) {
|
|
// Verify that providers can still accept inconsistent results from
|
|
// filesystem functions. We allow this for backwards compatibility, but
|
|
// ephemeral values should be used in the long-term to allow for controlled
|
|
// changes in values between plan and apply.
|
|
td := t.TempDir()
|
|
planDir := filepath.Join(td, "plan")
|
|
applyDir := filepath.Join(td, "apply")
|
|
testCopyDir(t, testFixturePath("changed-file-func-plan"), planDir)
|
|
testCopyDir(t, testFixturePath("changed-file-func-apply"), applyDir)
|
|
t.Chdir(planDir)
|
|
|
|
p := planVarsFixtureProvider()
|
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
|
Provider: providers.Schema{
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"foo": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_instance": {
|
|
Body: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Optional: true, Computed: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
view, done := testView(t)
|
|
c := &PlanCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
View: view,
|
|
},
|
|
}
|
|
|
|
args := []string{
|
|
"-out", filepath.Join(applyDir, "planfile"),
|
|
}
|
|
code := c.Run(args)
|
|
output := done(t)
|
|
if code != 0 {
|
|
t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr())
|
|
}
|
|
|
|
t.Chdir(applyDir)
|
|
|
|
view, done = testView(t)
|
|
apply := &ApplyCommand{
|
|
Meta: Meta{
|
|
testingOverrides: metaOverridesForProvider(p),
|
|
Ui: new(cli.MockUi),
|
|
View: view,
|
|
},
|
|
}
|
|
args = []string{
|
|
"planfile",
|
|
}
|
|
code = apply.Run(args)
|
|
output = done(t)
|
|
if code != 0 {
|
|
t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr())
|
|
}
|
|
}
|