2018-10-31 16:45:03 +01:00
|
|
|
package remote
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2020-05-02 01:01:36 +02:00
|
|
|
"github.com/google/go-cmp/cmp"
|
2018-10-31 16:45:03 +01:00
|
|
|
tfe "github.com/hashicorp/go-tfe"
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
version "github.com/hashicorp/go-version"
|
2021-05-17 21:00:50 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
2021-05-17 17:42:17 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/backend"
|
2021-05-17 21:07:38 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
|
|
|
"github.com/hashicorp/terraform/internal/command/clistate"
|
|
|
|
"github.com/hashicorp/terraform/internal/command/views"
|
2019-01-09 03:39:14 +01:00
|
|
|
"github.com/hashicorp/terraform/internal/initwd"
|
2021-05-17 21:33:17 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
|
|
"github.com/hashicorp/terraform/internal/plans/planfile"
|
2021-05-17 21:43:35 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/states/statemgr"
|
2021-02-16 13:19:22 +01:00
|
|
|
"github.com/hashicorp/terraform/internal/terminal"
|
2021-05-17 21:46:19 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
tfversion "github.com/hashicorp/terraform/version"
|
2018-10-31 16:45:03 +01:00
|
|
|
"github.com/mitchellh/cli"
|
|
|
|
)
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
|
2018-10-31 16:45:03 +01:00
|
|
|
t.Helper()
|
|
|
|
|
2021-02-16 13:19:22 +01:00
|
|
|
return testOperationApplyWithTimeout(t, configDir, 0)
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
|
2021-02-16 13:19:22 +01:00
|
|
|
t.Helper()
|
|
|
|
|
2019-01-09 03:39:14 +01:00
|
|
|
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
view := views.NewView(streams)
|
|
|
|
stateLockerView := views.NewStateLocker(arguments.ViewHuman, view)
|
|
|
|
operationView := views.NewOperation(arguments.ViewHuman, false, view)
|
2021-02-16 13:19:22 +01:00
|
|
|
|
2018-10-31 16:45:03 +01:00
|
|
|
return &backend.Operation{
|
2021-02-25 16:02:23 +01:00
|
|
|
ConfigDir: configDir,
|
|
|
|
ConfigLoader: configLoader,
|
|
|
|
Parallelism: defaultParallelism,
|
|
|
|
PlanRefresh: true,
|
|
|
|
StateLocker: clistate.NewLocker(timeout, stateLockerView),
|
|
|
|
Type: backend.OperationTypeApply,
|
|
|
|
View: operationView,
|
|
|
|
}, configCleanup, done
|
2021-02-12 19:59:14 +01:00
|
|
|
}
|
|
|
|
|
2018-10-31 16:45:03 +01:00
|
|
|
func TestRemote_applyBasic(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) > 0 {
|
|
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
2020-08-11 17:23:42 +02:00
|
|
|
|
|
|
|
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
|
|
|
|
// An error suggests that the state was not unlocked after apply
|
|
|
|
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
|
|
|
|
t.Fatalf("unexpected error locking state after apply: %s", err.Error())
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
|
2019-02-26 19:12:53 +01:00
|
|
|
func TestRemote_applyCanceled(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2019-02-26 19:12:53 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2019-02-26 19:12:53 +01:00
|
|
|
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop the run to simulate a Ctrl-C.
|
|
|
|
run.Stop()
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
2020-08-11 17:23:42 +02:00
|
|
|
|
|
|
|
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
|
|
|
|
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
|
|
|
|
t.Fatalf("unexpected error locking state after cancelling apply: %s", err.Error())
|
|
|
|
}
|
2019-02-26 19:12:53 +01:00
|
|
|
}
|
|
|
|
|
2018-10-31 16:45:03 +01:00
|
|
|
func TestRemote_applyWithoutPermissions(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendNoDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
// Create a named workspace without permissions.
|
|
|
|
w, err := b.client.Workspaces.Create(
|
|
|
|
context.Background(),
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
|
|
|
Name: tfe.String(b.prefix + "prod"),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
2019-02-26 19:12:53 +01:00
|
|
|
w.Permissions.CanQueueApply = false
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = "prod"
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(errOutput, "Insufficient rights to apply changes") {
|
|
|
|
t.Fatalf("expected a permissions error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithVCS(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendNoDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
// Create a named workspace with a VCS.
|
|
|
|
_, err := b.client.Workspaces.Create(
|
|
|
|
context.Background(),
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
|
|
|
Name: tfe.String(b.prefix + "prod"),
|
|
|
|
VCSRepo: &tfe.VCSRepoOptions{},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
op.Workspace = "prod"
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") {
|
|
|
|
t.Fatalf("expected a VCS error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithParallelism(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
op.Parallelism = 3
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(errOutput, "parallelism values are currently not supported") {
|
|
|
|
t.Fatalf("expected a parallelism error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithPlan(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
op.PlanFile = &planfile.Reader{}
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(errOutput, "saved plan is currently not supported") {
|
|
|
|
t.Fatalf("expected a saved plan error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithoutRefresh(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-05-14 06:28:16 +02:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
op.PlanRefresh = false
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
2021-05-14 06:28:16 +02:00
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be non-empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
// We should find a run inside the mock client that has refresh set
|
|
|
|
// to false.
|
|
|
|
runsAPI := b.client.Runs.(*mockRuns)
|
|
|
|
if got, want := len(runsAPI.runs), 1; got != want {
|
|
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
|
|
}
|
|
|
|
for _, run := range runsAPI.runs {
|
|
|
|
if diff := cmp.Diff(false, run.Refresh); diff != "" {
|
|
|
|
t.Errorf("wrong Refresh setting in the created run\n%s", diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithoutRefreshIncompatibleAPIVersion(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
b.client.SetFakeRemoteAPIVersion("2.3")
|
|
|
|
|
|
|
|
op.PlanRefresh = false
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
output := done(t)
|
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
errOutput := output.Stderr()
|
|
|
|
if !strings.Contains(errOutput, "Planning without refresh is not supported") {
|
|
|
|
t.Fatalf("expected a not supported error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithRefreshOnly(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
op.PlanMode = plans.RefreshOnlyMode
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be non-empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
// We should find a run inside the mock client that has refresh-only set
|
|
|
|
// to true.
|
|
|
|
runsAPI := b.client.Runs.(*mockRuns)
|
|
|
|
if got, want := len(runsAPI.runs), 1; got != want {
|
|
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
|
|
}
|
|
|
|
for _, run := range runsAPI.runs {
|
|
|
|
if diff := cmp.Diff(true, run.RefreshOnly); diff != "" {
|
|
|
|
t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
b.client.SetFakeRemoteAPIVersion("2.3")
|
|
|
|
|
|
|
|
op.PlanMode = plans.RefreshOnlyMode
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
2018-10-31 16:45:03 +01:00
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
2021-05-14 06:28:16 +02:00
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2021-05-14 06:28:16 +02:00
|
|
|
if !strings.Contains(errOutput, "Refresh-only mode is not supported") {
|
|
|
|
t.Fatalf("expected a not supported error, got: %v", errOutput)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithTarget(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
|
|
|
|
|
|
|
|
op.Targets = []addrs.Targetable{addr}
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2020-05-02 01:01:36 +02:00
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to succeed")
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
2020-05-02 01:01:36 +02:00
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be non-empty")
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
|
2020-05-02 01:01:36 +02:00
|
|
|
// We should find a run inside the mock client that has the same
|
|
|
|
// target address we requested above.
|
|
|
|
runsAPI := b.client.Runs.(*mockRuns)
|
|
|
|
if got, want := len(runsAPI.runs), 1; got != want {
|
|
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
|
|
}
|
|
|
|
for _, run := range runsAPI.runs {
|
|
|
|
if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" {
|
|
|
|
t.Errorf("wrong TargetAddrs in the created run\n%s", diff)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-19 18:39:15 +02:00
|
|
|
func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2020-05-19 18:39:15 +02:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
// Set the tfe client's RemoteAPIVersion to an empty string, to mimic
|
|
|
|
// API versions prior to 2.3.
|
|
|
|
b.client.SetFakeRemoteAPIVersion("")
|
|
|
|
|
|
|
|
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
|
|
|
|
|
|
|
|
op.Targets = []addrs.Targetable{addr}
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2020-05-19 18:39:15 +02:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2020-05-19 18:39:15 +02:00
|
|
|
if !strings.Contains(errOutput, "Resource targeting is not supported") {
|
|
|
|
t.Fatalf("expected a targeting error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-14 06:28:16 +02:00
|
|
|
func TestRemote_applyWithReplace(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo")
|
|
|
|
|
|
|
|
op.ForceReplace = []addrs.AbsResourceInstance{addr}
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatal("expected plan operation to succeed")
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be non-empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
// We should find a run inside the mock client that has the same
|
|
|
|
// refresh address we requested above.
|
|
|
|
runsAPI := b.client.Runs.(*mockRuns)
|
|
|
|
if got, want := len(runsAPI.runs), 1; got != want {
|
|
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
|
|
}
|
|
|
|
for _, run := range runsAPI.runs {
|
|
|
|
if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" {
|
|
|
|
t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithReplaceIncompatibleAPIVersion(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
b.client.SetFakeRemoteAPIVersion("2.3")
|
|
|
|
|
|
|
|
addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo")
|
|
|
|
|
|
|
|
op.ForceReplace = []addrs.AbsResourceInstance{addr}
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
output := done(t)
|
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
errOutput := output.Stderr()
|
|
|
|
if !strings.Contains(errOutput, "Planning resource replacements is not supported") {
|
|
|
|
t.Fatalf("expected a not supported error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-31 16:45:03 +01:00
|
|
|
func TestRemote_applyWithVariables(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-variables")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
op.Variables = testVariables(terraform.ValueFromNamedFile, "foo", "bar")
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(errOutput, "variables are currently not supported") {
|
|
|
|
t.Fatalf("expected a variables error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyNoConfig(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/empty")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(errOutput, "configuration files found") {
|
|
|
|
t.Fatalf("expected configuration files error, got: %v", errOutput)
|
|
|
|
}
|
2020-08-11 17:23:42 +02:00
|
|
|
|
|
|
|
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
|
|
|
|
// An error suggests that the state was not unlocked after apply
|
|
|
|
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
|
|
|
|
t.Fatalf("unexpected error locking state after failed apply: %s", err.Error())
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyNoChanges(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-no-changes")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") {
|
|
|
|
t.Fatalf("expected no changes in plan summery: %s", output)
|
|
|
|
}
|
2018-11-19 19:09:37 +01:00
|
|
|
if !strings.Contains(output, "Sentinel Result: true") {
|
|
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyNoApprove(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "no",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) > 0 {
|
|
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(errOutput, "Apply discarded") {
|
|
|
|
t.Fatalf("expected an apply discarded error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyAutoApprove(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "no",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.AutoApprove = true
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) != 1 {
|
|
|
|
t.Fatalf("expected an unused answer, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-08 10:37:25 +01:00
|
|
|
func TestRemote_applyApprovedExternally(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2019-03-08 10:37:25 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2019-03-08 10:37:25 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "wait-for-external-update",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
run, err := b.Operation(ctx, op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-11-18 21:35:48 +01:00
|
|
|
// Wait 50 milliseconds to make sure the run started.
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
2019-03-08 10:37:25 +01:00
|
|
|
|
|
|
|
wl, err := b.client.Workspaces.List(
|
|
|
|
ctx,
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceListOptions{
|
|
|
|
ListOptions: tfe.ListOptions{PageNumber: 2, PageSize: 10},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error listing workspaces: %v", err)
|
|
|
|
}
|
|
|
|
if len(wl.Items) != 1 {
|
|
|
|
t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items))
|
|
|
|
}
|
|
|
|
|
|
|
|
rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, tfe.RunListOptions{})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error listing runs: %v", err)
|
|
|
|
}
|
|
|
|
if len(rl.Items) != 1 {
|
|
|
|
t.Fatalf("expected 1 run, got %d runs", len(rl.Items))
|
|
|
|
}
|
|
|
|
|
|
|
|
err = b.client.Runs.Apply(context.Background(), rl.Items[0].ID, tfe.RunApplyOptions{})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error approving run: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
|
|
}
|
|
|
|
if !strings.Contains(output, "approved using the UI or API") {
|
|
|
|
t.Fatalf("expected external approval in output: %s", output)
|
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyDiscardedExternally(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2019-03-08 10:37:25 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2019-03-08 10:37:25 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "wait-for-external-update",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
run, err := b.Operation(ctx, op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-11-18 21:35:48 +01:00
|
|
|
// Wait 50 milliseconds to make sure the run started.
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
2019-03-08 10:37:25 +01:00
|
|
|
|
|
|
|
wl, err := b.client.Workspaces.List(
|
|
|
|
ctx,
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceListOptions{
|
|
|
|
ListOptions: tfe.ListOptions{PageNumber: 2, PageSize: 10},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error listing workspaces: %v", err)
|
|
|
|
}
|
|
|
|
if len(wl.Items) != 1 {
|
|
|
|
t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items))
|
|
|
|
}
|
|
|
|
|
|
|
|
rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, tfe.RunListOptions{})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error listing runs: %v", err)
|
|
|
|
}
|
|
|
|
if len(rl.Items) != 1 {
|
|
|
|
t.Fatalf("expected 1 run, got %d runs", len(rl.Items))
|
|
|
|
}
|
|
|
|
|
|
|
|
err = b.client.Runs.Discard(context.Background(), rl.Items[0].ID, tfe.RunDiscardOptions{})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error discarding run: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
|
|
}
|
|
|
|
if !strings.Contains(output, "discarded using the UI or API") {
|
|
|
|
t.Fatalf("expected external discard output: %s", output)
|
|
|
|
}
|
|
|
|
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
|
|
t.Fatalf("unexpected apply summery in output: %s", output)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-31 16:45:03 +01:00
|
|
|
func TestRemote_applyWithAutoApply(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendNoDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
// Create a named workspace that auto applies.
|
|
|
|
_, err := b.client.Workspaces.Create(
|
|
|
|
context.Background(),
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
|
|
|
AutoApply: tfe.Bool(true),
|
|
|
|
Name: tfe.String(b.prefix + "prod"),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = "prod"
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) != 1 {
|
|
|
|
t.Fatalf("expected an unused answer, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyForceLocal(t *testing.T) {
|
|
|
|
// Set TF_FORCE_LOCAL_BACKEND so the remote backend will use
|
|
|
|
// the local backend with itself as embedded backend.
|
|
|
|
if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
|
|
|
|
t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
|
|
|
|
}
|
|
|
|
defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
|
|
|
|
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-11-15 20:26:46 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-11-15 20:26:46 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-11-15 20:26:46 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
backend/local: Replace CLI with view instance
This commit extracts the remaining UI logic from the local backend,
and removes access to the direct CLI output. This is replaced with an
instance of a `views.Operation` interface, which codifies the current
requirements for the local backend to interact with the user.
The exception to this at present is interactivity: approving a plan
still depends on the `UIIn` field for the backend. This is out of scope
for this commit and can be revisited separately, at which time the
`UIOut` field can also be removed.
Changes in support of this:
- Some instances of direct error output have been replaced with
diagnostics, most notably in the emergency state backup handler. This
requires reformatting the error messages to allow the diagnostic
renderer to line-wrap them;
- The "in-automation" logic has moved out of the backend and into the
view implementation;
- The plan, apply, refresh, and import commands instantiate a view and
set it on the `backend.Operation` struct, as these are the only code
paths which call the `local.Operation()` method that requires it;
- The show command requires the plan rendering code which is now in the
views package, so there is a stub implementation of a `views.Show`
interface there.
Other refactoring work in support of migrating these commands to the
common views code structure will come in follow-up PRs, at which point
we will be able to remove the UI instances from the unit tests for those
commands.
2021-02-17 19:01:30 +01:00
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
|
|
|
|
op.View = view
|
|
|
|
|
2018-11-15 20:26:46 +01:00
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) > 0 {
|
|
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("unexpected remote backend header in output: %s", output)
|
|
|
|
}
|
backend/local: Replace CLI with view instance
This commit extracts the remaining UI logic from the local backend,
and removes access to the direct CLI output. This is replaced with an
instance of a `views.Operation` interface, which codifies the current
requirements for the local backend to interact with the user.
The exception to this at present is interactivity: approving a plan
still depends on the `UIIn` field for the backend. This is out of scope
for this commit and can be revisited separately, at which time the
`UIOut` field can also be removed.
Changes in support of this:
- Some instances of direct error output have been replaced with
diagnostics, most notably in the emergency state backup handler. This
requires reformatting the error messages to allow the diagnostic
renderer to line-wrap them;
- The "in-automation" logic has moved out of the backend and into the
view implementation;
- The plan, apply, refresh, and import commands instantiate a view and
set it on the `backend.Operation` struct, as these are the only code
paths which call the `local.Operation()` method that requires it;
- The show command requires the plan rendering code which is now in the
views package, so there is a stub implementation of a `views.Show`
interface there.
Other refactoring work in support of migrating these commands to the
common views code structure will come in follow-up PRs, at which point
we will be able to remove the UI instances from the unit tests for those
commands.
2021-02-17 19:01:30 +01:00
|
|
|
if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
|
|
t.Fatalf("expected plan summary in output: %s", output)
|
2018-11-15 20:26:46 +01:00
|
|
|
}
|
2021-01-29 20:39:06 +01:00
|
|
|
if !run.State.HasResources() {
|
|
|
|
t.Fatalf("expected resources in state")
|
2018-11-15 20:26:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWorkspaceWithoutOperations(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendNoDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
2018-11-15 20:26:46 +01:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// Create a named workspace that doesn't allow operations.
|
|
|
|
_, err := b.client.Workspaces.Create(
|
|
|
|
ctx,
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
|
|
|
Name: tfe.String(b.prefix + "no-operations"),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
2018-11-15 20:26:46 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-11-15 20:26:46 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = "no-operations"
|
|
|
|
|
backend/local: Replace CLI with view instance
This commit extracts the remaining UI logic from the local backend,
and removes access to the direct CLI output. This is replaced with an
instance of a `views.Operation` interface, which codifies the current
requirements for the local backend to interact with the user.
The exception to this at present is interactivity: approving a plan
still depends on the `UIIn` field for the backend. This is out of scope
for this commit and can be revisited separately, at which time the
`UIOut` field can also be removed.
Changes in support of this:
- Some instances of direct error output have been replaced with
diagnostics, most notably in the emergency state backup handler. This
requires reformatting the error messages to allow the diagnostic
renderer to line-wrap them;
- The "in-automation" logic has moved out of the backend and into the
view implementation;
- The plan, apply, refresh, and import commands instantiate a view and
set it on the `backend.Operation` struct, as these are the only code
paths which call the `local.Operation()` method that requires it;
- The show command requires the plan rendering code which is now in the
views package, so there is a stub implementation of a `views.Show`
interface there.
Other refactoring work in support of migrating these commands to the
common views code structure will come in follow-up PRs, at which point
we will be able to remove the UI instances from the unit tests for those
commands.
2021-02-17 19:01:30 +01:00
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
|
|
|
|
op.View = view
|
|
|
|
|
2018-11-15 20:26:46 +01:00
|
|
|
run, err := b.Operation(ctx, op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) > 0 {
|
|
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("unexpected remote backend header in output: %s", output)
|
|
|
|
}
|
backend/local: Replace CLI with view instance
This commit extracts the remaining UI logic from the local backend,
and removes access to the direct CLI output. This is replaced with an
instance of a `views.Operation` interface, which codifies the current
requirements for the local backend to interact with the user.
The exception to this at present is interactivity: approving a plan
still depends on the `UIIn` field for the backend. This is out of scope
for this commit and can be revisited separately, at which time the
`UIOut` field can also be removed.
Changes in support of this:
- Some instances of direct error output have been replaced with
diagnostics, most notably in the emergency state backup handler. This
requires reformatting the error messages to allow the diagnostic
renderer to line-wrap them;
- The "in-automation" logic has moved out of the backend and into the
view implementation;
- The plan, apply, refresh, and import commands instantiate a view and
set it on the `backend.Operation` struct, as these are the only code
paths which call the `local.Operation()` method that requires it;
- The show command requires the plan rendering code which is now in the
views package, so there is a stub implementation of a `views.Show`
interface there.
Other refactoring work in support of migrating these commands to the
common views code structure will come in follow-up PRs, at which point
we will be able to remove the UI instances from the unit tests for those
commands.
2021-02-17 19:01:30 +01:00
|
|
|
if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
|
|
t.Fatalf("expected plan summary in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
2021-01-29 20:39:06 +01:00
|
|
|
if !run.State.HasResources() {
|
|
|
|
t.Fatalf("expected resources in state")
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyLockTimeout(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
2018-10-31 16:45:03 +01:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// Retrieve the workspace used to run this operation in.
|
|
|
|
w, err := b.client.Workspaces.Read(ctx, b.organization, b.workspace)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error retrieving workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a new configuration version.
|
|
|
|
c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating configuration version: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a pending run to block this run.
|
|
|
|
_, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{
|
|
|
|
ConfigurationVersion: c,
|
|
|
|
Workspace: w,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating pending run: %v", err)
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApplyWithTimeout(t, "./testdata/apply", 50*time.Millisecond)
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"cancel": "yes",
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
_, err = b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
sigint := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(sigint, syscall.SIGINT)
|
|
|
|
select {
|
|
|
|
case <-sigint:
|
|
|
|
// Stop redirecting SIGINT signals.
|
|
|
|
signal.Stop(sigint)
|
2020-11-18 21:35:48 +01:00
|
|
|
case <-time.After(200 * time.Millisecond):
|
|
|
|
t.Fatalf("expected lock timeout after 50 milliseconds, waited 200 milliseconds")
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) != 2 {
|
|
|
|
t.Fatalf("expected unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(output, "Lock timeout exceeded") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected lock timout error in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
|
|
t.Fatalf("unexpected plan summery in output: %s", output)
|
|
|
|
}
|
|
|
|
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
|
|
t.Fatalf("unexpected apply summery in output: %s", output)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyDestroy(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-destroy")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
2021-04-06 01:28:59 +02:00
|
|
|
op.PlanMode = plans.DestroyMode
|
2018-10-31 16:45:03 +01:00
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) > 0 {
|
|
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyDestroyNoConfig(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/empty")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-04-06 01:28:59 +02:00
|
|
|
op.PlanMode = plans.DestroyMode
|
2018-10-31 16:45:03 +01:00
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) > 0 {
|
|
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyPolicyPass(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-passed")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) > 0 {
|
|
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "Sentinel Result: true") {
|
2018-11-19 19:09:37 +01:00
|
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyPolicyHardFail(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-hard-failed")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
viewOutput := done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if !run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) != 1 {
|
|
|
|
t.Fatalf("expected an unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := viewOutput.Stderr()
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(errOutput, "hard failed") {
|
|
|
|
t.Fatalf("expected a policy check error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "Sentinel Result: false") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
|
|
t.Fatalf("unexpected apply summery in output: %s", output)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyPolicySoftFail(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"override": "override",
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
2021-03-01 14:54:30 +01:00
|
|
|
op.AutoApprove = false
|
2018-10-31 16:45:03 +01:00
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) > 0 {
|
|
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "Sentinel Result: false") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-01 14:54:30 +01:00
|
|
|
func TestRemote_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
2021-03-01 14:54:30 +01:00
|
|
|
input := testInput(t, map[string]string{})
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
op.AutoApprove = true
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
viewOutput := done(t)
|
2021-03-01 14:54:30 +01:00
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to success due to auto-approve")
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
2021-03-01 14:54:30 +01:00
|
|
|
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected plan to not be empty, plan opertion completed without error")
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
|
2021-03-01 14:54:30 +01:00
|
|
|
if len(input.answers) != 0 {
|
|
|
|
t.Fatalf("expected no answers, got: %v", input.answers)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := viewOutput.Stderr()
|
2021-03-01 14:54:30 +01:00
|
|
|
if strings.Contains(errOutput, "soft failed") {
|
|
|
|
t.Fatalf("expected no policy check errors, instead got: %v", errOutput)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if !strings.Contains(output, "Sentinel Result: false") {
|
2021-03-01 14:54:30 +01:00
|
|
|
t.Fatalf("expected policy check to be false, insead got: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
2021-03-01 14:54:30 +01:00
|
|
|
if !strings.Contains(output, "Apply complete!") {
|
|
|
|
t.Fatalf("expected apply to be complete, instead got: %s", output)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !strings.Contains(output, "Resources: 1 added, 0 changed, 0 destroyed") {
|
|
|
|
t.Fatalf("expected resources, instead got: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyPolicySoftFailAutoApply(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
// Create a named workspace that auto applies.
|
|
|
|
_, err := b.client.Workspaces.Create(
|
|
|
|
context.Background(),
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
|
|
|
AutoApply: tfe.Bool(true),
|
|
|
|
Name: tfe.String(b.prefix + "prod"),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"override": "override",
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = "prod"
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
|
|
t.Fatalf("expected a non-empty plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(input.answers) != 1 {
|
|
|
|
t.Fatalf("expected an unused answer, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
2018-11-15 20:26:46 +01:00
|
|
|
if !strings.Contains(output, "Running apply in the remote backend") {
|
|
|
|
t.Fatalf("expected remote backend header in output: %s", output)
|
|
|
|
}
|
2018-10-31 16:45:03 +01:00
|
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "Sentinel Result: false") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemote_applyWithRemoteError(t *testing.T) {
|
2019-02-06 09:36:42 +01:00
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
2018-10-31 16:45:03 +01:00
|
|
|
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-with-error")
|
2018-10-31 16:45:03 +01:00
|
|
|
defer configCleanup()
|
2021-02-25 16:02:23 +01:00
|
|
|
defer done(t)
|
2018-10-31 16:45:03 +01:00
|
|
|
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
|
|
|
if run.Result == backend.OperationSuccess {
|
|
|
|
t.Fatal("expected apply operation to fail")
|
|
|
|
}
|
|
|
|
if run.Result.ExitStatus() != 1 {
|
|
|
|
t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus())
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if !strings.Contains(output, "null_resource.foo: 1 error") {
|
2018-11-15 20:26:46 +01:00
|
|
|
t.Fatalf("expected apply error in output: %s", output)
|
2018-10-31 16:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
|
|
|
|
func TestRemote_applyVersionCheck(t *testing.T) {
|
|
|
|
testCases := map[string]struct {
|
|
|
|
localVersion string
|
|
|
|
remoteVersion string
|
|
|
|
forceLocal bool
|
|
|
|
hasOperations bool
|
|
|
|
wantErr string
|
|
|
|
}{
|
|
|
|
"versions can be different for remote apply": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.13.5",
|
|
|
|
hasOperations: true,
|
|
|
|
},
|
|
|
|
"versions can be different for local apply": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.13.5",
|
|
|
|
hasOperations: false,
|
|
|
|
},
|
2020-12-17 18:56:13 +01:00
|
|
|
"force local with remote operations and different versions is acceptable": {
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
localVersion: "0.14.0",
|
2020-12-17 18:56:13 +01:00
|
|
|
remoteVersion: "0.14.0-acme-provider-bundle",
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
forceLocal: true,
|
|
|
|
hasOperations: true,
|
|
|
|
},
|
|
|
|
"no error if versions are identical": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.14.0",
|
|
|
|
forceLocal: true,
|
|
|
|
hasOperations: true,
|
|
|
|
},
|
|
|
|
"no error if force local but workspace has remote operations disabled": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.13.5",
|
|
|
|
forceLocal: true,
|
|
|
|
hasOperations: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
b, bCleanup := testBackendDefault(t)
|
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
// SETUP: Save original local version state and restore afterwards
|
|
|
|
p := tfversion.Prerelease
|
|
|
|
v := tfversion.Version
|
|
|
|
s := tfversion.SemVer
|
|
|
|
defer func() {
|
|
|
|
tfversion.Prerelease = p
|
|
|
|
tfversion.Version = v
|
|
|
|
tfversion.SemVer = s
|
|
|
|
}()
|
|
|
|
|
|
|
|
// SETUP: Set local version for the test case
|
|
|
|
tfversion.Prerelease = ""
|
|
|
|
tfversion.Version = tc.localVersion
|
|
|
|
tfversion.SemVer = version.Must(version.NewSemver(tc.localVersion))
|
|
|
|
|
|
|
|
// SETUP: Set force local for the test case
|
|
|
|
b.forceLocal = tc.forceLocal
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// SETUP: set the operations and Terraform Version fields on the
|
|
|
|
// remote workspace
|
|
|
|
_, err := b.client.Workspaces.Update(
|
|
|
|
ctx,
|
|
|
|
b.organization,
|
|
|
|
b.workspace,
|
|
|
|
tfe.WorkspaceUpdateOptions{
|
|
|
|
Operations: tfe.Bool(tc.hasOperations),
|
|
|
|
TerraformVersion: tfe.String(tc.remoteVersion),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RUN: prepare the apply operation and run it
|
2021-02-25 16:02:23 +01:00
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
defer configCleanup()
|
|
|
|
|
backend/local: Replace CLI with view instance
This commit extracts the remaining UI logic from the local backend,
and removes access to the direct CLI output. This is replaced with an
instance of a `views.Operation` interface, which codifies the current
requirements for the local backend to interact with the user.
The exception to this at present is interactivity: approving a plan
still depends on the `UIIn` field for the backend. This is out of scope
for this commit and can be revisited separately, at which time the
`UIOut` field can also be removed.
Changes in support of this:
- Some instances of direct error output have been replaced with
diagnostics, most notably in the emergency state backup handler. This
requires reformatting the error messages to allow the diagnostic
renderer to line-wrap them;
- The "in-automation" logic has moved out of the backend and into the
view implementation;
- The plan, apply, refresh, and import commands instantiate a view and
set it on the `backend.Operation` struct, as these are the only code
paths which call the `local.Operation()` method that requires it;
- The show command requires the plan rendering code which is now in the
views package, so there is a stub implementation of a `views.Show`
interface there.
Other refactoring work in support of migrating these commands to the
common views code structure will come in follow-up PRs, at which point
we will be able to remove the UI instances from the unit tests for those
commands.
2021-02-17 19:01:30 +01:00
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
|
|
|
|
op.View = view
|
|
|
|
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = backend.DefaultStateName
|
|
|
|
|
|
|
|
run, err := b.Operation(ctx, op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RUN: wait for completion
|
|
|
|
<-run.Done()
|
2021-02-25 16:02:23 +01:00
|
|
|
output := done(t)
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
|
|
|
|
if tc.wantErr != "" {
|
|
|
|
// ASSERT: if the test case wants an error, check for failure
|
|
|
|
// and the error message
|
|
|
|
if run.Result != backend.OperationFailure {
|
|
|
|
t.Fatalf("expected run to fail, but result was %#v", run.Result)
|
|
|
|
}
|
2021-02-25 16:02:23 +01:00
|
|
|
errOutput := output.Stderr()
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
if !strings.Contains(errOutput, tc.wantErr) {
|
|
|
|
t.Fatalf("missing error %q\noutput: %s", tc.wantErr, errOutput)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// ASSERT: otherwise, check for success and appropriate output
|
|
|
|
// based on whether the run should be local or remote
|
|
|
|
if run.Result != backend.OperationSuccess {
|
|
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
hasRemote := strings.Contains(output, "Running apply in the remote backend")
|
2021-01-29 20:39:06 +01:00
|
|
|
hasSummary := strings.Contains(output, "1 added, 0 changed, 0 destroyed")
|
|
|
|
hasResources := run.State.HasResources()
|
|
|
|
if !tc.forceLocal && tc.hasOperations {
|
|
|
|
if !hasRemote {
|
|
|
|
t.Errorf("missing remote backend header in output: %s", output)
|
|
|
|
}
|
|
|
|
if !hasSummary {
|
|
|
|
t.Errorf("expected apply summary in output: %s", output)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if hasRemote {
|
|
|
|
t.Errorf("unexpected remote backend header in output: %s", output)
|
|
|
|
}
|
|
|
|
if !hasResources {
|
|
|
|
t.Errorf("expected resources in state")
|
|
|
|
}
|
backend: Validate remote backend Terraform version
When using the enhanced remote backend, a subset of all Terraform
operations are supported. Of these, only plan and apply can be executed
on the remote infrastructure (e.g. Terraform Cloud). Other operations
run locally and use the remote backend for state storage.
This causes problems when the local version of Terraform does not match
the configured version from the remote workspace. If the two versions
are incompatible, an `import` or `state mv` operation can cause the
remote workspace to be unusable until a manual fix is applied.
To prevent this from happening accidentally, this commit introduces a
check that the local Terraform version and the configured remote
workspace Terraform version are compatible. This check is skipped for
commands which do not write state, and can also be disabled by the use
of a new command-line flag, `-ignore-remote-version`.
Terraform version compatibility is defined as:
- For all releases before 0.14.0, local must exactly equal remote, as
two different versions cannot share state;
- 0.14.0 to 1.0.x are compatible, as we will not change the state
version number until at least Terraform 1.1.0;
- Versions after 1.1.0 must have the same major and minor versions, as
we will not change the state version number in a patch release.
If the two versions are incompatible, a diagnostic is displayed,
advising that the error can be suppressed with `-ignore-remote-version`.
When this flag is used, the diagnostic is still displayed, but as a
warning instead of an error.
Commands which will not write state can assert this fact by calling the
helper `meta.ignoreRemoteBackendVersionConflict`, which will disable the
checks. Those which can write state should instead call the helper
`meta.remoteBackendVersionCheck`, which will return diagnostics for
display.
In addition to these explicit paths for managing the version check, we
have an implicit check in the remote backend's state manager
initialization method. Both of the above helpers will disable this
check. This fallback is in place to ensure that future code paths which
access state cannot accidentally skip the remote version check.
2020-11-13 22:43:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|