| // 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/command/clistate" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/getproviders" | |
| "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) | |
| _ = 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 := testUiWrapped(t, 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() | |
| _ = 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 := testUiWrapped(t, 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) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| t.Chdir(td) | |
| // 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, testStateStoreMock(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.") | |
| } | |
| } | |
| func TestApply_plan_stateStore_providerSupplyModes(t *testing.T) { | |
| t.Run("can run init, plan, apply workflow using a dev override provider for PSS", func(t *testing.T) { | |
| // Create a temporary, uninitialized working directory with configuration including a state store | |
| td := t.TempDir() | |
| t.Chdir(td) | |
| cfg := `terraform { | |
| required_providers { | |
| test = { | |
| source = "hashicorp/test" | |
| } | |
| } | |
| state_store "test_store" { | |
| provider "test" { | |
| } | |
| value = "foobar" | |
| } | |
| } | |
| output "foobar" { | |
| value = "ok" | |
| } | |
| ` | |
| if err := os.WriteFile("main.tf", []byte(cfg), 0644); err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| // Mock provider still needs to be supplied via testingOverrides despite the mock HTTP source | |
| mockProvider := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store")) | |
| mockProvider.MockStates = testing_provider.NewMockStateBytesWithStateIds("test_store", []string{"default"}) | |
| mockProviderAddress := addrs.NewDefaultProvider("test") | |
| source := newMockProviderSource(t, map[string][]string{ | |
| // The test fixture config has no version constraints, so the latest version will | |
| // be used; below is the 'latest' version in the test world. | |
| "hashicorp/test": {"1.2.3"}, | |
| }) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| meta := Meta{ | |
| Ui: ui, | |
| View: view, | |
| AllowExperimentalFeatures: true, | |
| testingOverrides: &testingOverrides{ | |
| Providers: map[addrs.Provider]providers.Factory{ | |
| mockProviderAddress: providers.FactoryFixed(mockProvider), | |
| }, | |
| }, | |
| ProviderSource: source, | |
| // THIS ALLOWS THE TEST TO MIMIC A SCENARIO WHERE THE PROVIDER IS A DEV OVERRIDE. | |
| // The mock is still accessed via testingOverrides, but Terraform believes that it's a dev override due to | |
| // its presence in the ProviderDevOverrides map. | |
| ProviderDevOverrides: map[addrs.Provider]getproviders.PackageLocalDir{ | |
| mockProviderAddress: ".", | |
| }, | |
| } | |
| // Init | |
| initCmd := &InitCommand{ | |
| Meta: meta, | |
| } | |
| args := []string{"-enable-pluggable-state-storage-experiment=true"} | |
| if code := initCmd.Run(args); code != 0 { | |
| t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, done(t).All()) | |
| } | |
| // Assert backend state file says the provider is a dev override | |
| statePath := filepath.Join(meta.DataDir(), DefaultStateFilename) | |
| sMgr := &clistate.LocalState{Path: statePath} | |
| if err := sMgr.RefreshState(); err != nil { | |
| t.Fatal("Failed to load state:", err) | |
| } | |
| s := sMgr.State() | |
| if s == nil || s.StateStore == nil { | |
| t.Fatal("expected backend state file to be created and include state store details, but it was missing.") | |
| } | |
| if s.StateStore.ProviderSupplyMode != getproviders.DevOverride { | |
| t.Fatalf("expected state store provider supply mode to be 'dev_override', got '%s'", s.StateStore.ProviderSupplyMode) | |
| } | |
| // Plan - to produce a plan file. | |
| // | |
| // The plan file will not include version data, as the provider is considered to be a dev override. | |
| ui = testUiWrapped(t) | |
| meta.Ui = ui | |
| view, done = testView(t) | |
| meta.View = view | |
| planCmd := &PlanCommand{ | |
| Meta: meta, | |
| } | |
| args = []string{ | |
| "-out=planfile", | |
| } | |
| if code := planCmd.Run(args); code != 0 { | |
| t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, done(t).All()) | |
| } | |
| // Apply - to use the plan file that lacks version data. | |
| ui = testUiWrapped(t) | |
| meta.Ui = ui | |
| view, done = testView(t) | |
| meta.View = view | |
| applyCmd := &ApplyCommand{ | |
| Meta: meta, | |
| } | |
| args = []string{ | |
| "planfile", | |
| } | |
| if code := applyCmd.Run(args); code != 0 { | |
| t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, done(t).All()) | |
| } | |
| }) | |
| t.Run("can run init, plan, apply workflow using a builtin provider for PSS", func(t *testing.T) { | |
| // Create a temporary, uninitialized working directory with configuration including a state store | |
| td := t.TempDir() | |
| t.Chdir(td) | |
| cfg := `terraform { | |
| required_providers { | |
| terraform = { | |
| source = "terraform.io/builtin/terraform" | |
| } | |
| } | |
| state_store "terraform_store" { | |
| provider "terraform" { | |
| } | |
| value = "foobar" | |
| } | |
| } | |
| output "foobar" { | |
| value = "ok" | |
| } | |
| ` | |
| if err := os.WriteFile("main.tf", []byte(cfg), 0644); err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| // THIS ALLOWS THE TEST TO MIMIC A SCENARIO WHERE THE PSS PROVIDER IS BUILTIN. | |
| // Terraform validates that builtin providers are called 'terraform' but we can still supply | |
| // a mock in place of the actual builtin terraform provider. We just need the address and any | |
| // schemas to match the builtin provider. | |
| mockProvider := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store")) | |
| mockProvider.MockStates = testing_provider.NewMockStateBytesWithTypesAndStateIds( | |
| []string{"test_store", "terraform_store"}, | |
| []string{"default"}, | |
| ) | |
| schema := mockProvider.GetProviderSchema() | |
| schema.StateStores["terraform_store"] = schema.StateStores["test_store"] // rename to match pretending this is a store in the builtin terraform provider. | |
| mockProvider.GetProviderSchemaResponse = &schema | |
| mockProviderAddress := addrs.NewBuiltInProvider("terraform") | |
| source := newMockProviderSource(t, map[string][]string{ | |
| "hashicorp/terraform": {"1.2.3"}, | |
| }) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| meta := Meta{ | |
| Ui: ui, | |
| View: view, | |
| AllowExperimentalFeatures: true, | |
| testingOverrides: &testingOverrides{ | |
| Providers: map[addrs.Provider]providers.Factory{ | |
| mockProviderAddress: providers.FactoryFixed(mockProvider), | |
| }, | |
| }, | |
| ProviderSource: source, | |
| } | |
| // Init | |
| initCmd := &InitCommand{ | |
| Meta: meta, | |
| } | |
| args := []string{"-enable-pluggable-state-storage-experiment=true"} | |
| if code := initCmd.Run(args); code != 0 { | |
| t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, done(t).All()) | |
| } | |
| // Assert backend state file says the provider is built in | |
| statePath := filepath.Join(meta.DataDir(), DefaultStateFilename) | |
| sMgr := &clistate.LocalState{Path: statePath} | |
| if err := sMgr.RefreshState(); err != nil { | |
| t.Fatal("Failed to load state:", err) | |
| } | |
| s := sMgr.State() | |
| if s == nil || s.StateStore == nil { | |
| t.Fatal("expected backend state file to be created and include state store details, but it was missing.") | |
| } | |
| if s.StateStore.ProviderSupplyMode != getproviders.BuiltIn { | |
| t.Fatalf("expected state store provider supply mode to be 'built_in', got '%s'", s.StateStore.ProviderSupplyMode) | |
| } | |
| // Plan - to produce a plan file. | |
| // | |
| // The plan file will not include version data, as the provider is considered to be a dev override. | |
| ui = testUiWrapped(t) | |
| meta.Ui = ui | |
| view, done = testView(t) | |
| meta.View = view | |
| planCmd := &PlanCommand{ | |
| Meta: meta, | |
| } | |
| args = []string{ | |
| "-out=planfile", | |
| } | |
| if code := planCmd.Run(args); code != 0 { | |
| t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, done(t).All()) | |
| } | |
| // Apply - to use the plan file that lacks version data. | |
| ui = testUiWrapped(t) | |
| meta.Ui = ui | |
| view, done = testView(t) | |
| meta.View = view | |
| applyCmd := &ApplyCommand{ | |
| Meta: meta, | |
| } | |
| args = []string{ | |
| "planfile", | |
| } | |
| if code := applyCmd.Run(args); code != 0 { | |
| t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, done(t).All()) | |
| } | |
| }) | |
| // See E2E tests for testing swapping between unmanaged(reattached and dev_override) and managed providers across the workflow. | |
| } | |
| // 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, testStateStoreMock(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_planWithVars(t *testing.T) { | |
| // Regression test for apply with a plan file that includes variables, | |
| // to ensure that apply doesn't prompt for the variables | |
| tDir := testTempDir(t) | |
| // The value of foo is the same as in the var file | |
| planPath := applyFixturePlanFileWithVariableValue(t, "bar") | |
| statePath := testTempFile(t) | |
| t.Chdir(tDir) | |
| 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_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 := testUiWrapped(t) | |
| 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 := testUiWrapped(t) | |
| 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()) | |
| } | |
| } |