package remote import ( "context" "os" "os/signal" "strings" "syscall" "testing" "time" tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) { t.Helper() _, configLoader, configCleanup := configload.MustLoadConfigForTests(t, configDir) return &backend.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, Parallelism: defaultParallelism, PlanRefresh: true, Type: backend.OperationTypePlan, }, configCleanup } func TestRemote_planBasic(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") 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() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatal("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan 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) } } func TestRemote_planWithoutPermissions(t *testing.T) { b := testBackendNoDefault(t) // 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) } w.Permissions.CanQueueRun = false op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") defer configCleanup() 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.Fatal("expected plan operation to fail") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "Insufficient rights to generate a plan") { t.Fatalf("expected a permissions error, got: %v", errOutput) } } func TestRemote_planWithParallelism(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") 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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "parallelism values are currently not supported") { t.Fatalf("expected a parallelism error, got: %v", errOutput) } } func TestRemote_planWithPlan(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") 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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "saved plan is currently not supported") { t.Fatalf("expected a saved plan error, got: %v", errOutput) } } func TestRemote_planWithPath(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") defer configCleanup() op.PlanOutPath = "./test-fixtures/plan" 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 fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "generated plan is currently not supported") { t.Fatalf("expected a generated plan error, got: %v", errOutput) } } func TestRemote_planWithoutRefresh(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") defer configCleanup() 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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "refresh is currently not supported") { t.Fatalf("expected a refresh error, got: %v", errOutput) } } func TestRemote_planWithTarget(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") defer configCleanup() 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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "targeting is currently not supported") { t.Fatalf("expected a targeting error, got: %v", errOutput) } } func TestRemote_planWithVariables(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-variables") defer configCleanup() op.Variables = testVariables(terraform.ValueFromCLIArg, "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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "variables are currently not supported") { t.Fatalf("expected a variables error, got: %v", errOutput) } } func TestRemote_planNoConfig(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/empty") 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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "configuration files found") { t.Fatalf("expected configuration files error, got: %v", errOutput) } } func TestRemote_planForceLocal(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") b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") 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() 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 plan in the remote backend") { t.Fatalf("unexpected 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) } } func TestRemote_planWorkspaceWithoutOperations(t *testing.T) { b := testBackendNoDefault(t) 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) } op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") defer configCleanup() op.Workspace = "no-operations" 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") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if strings.Contains(output, "Running plan in the remote backend") { t.Fatalf("unexpected 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) } } func TestRemote_planLockTimeout(t *testing.T) { b := testBackendDefault(t) 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) } op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") defer configCleanup() input := testInput(t, map[string]string{ "cancel": "yes", "approve": "yes", }) op.StateLockTimeout = 5 * time.Second 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) case <-time.After(10 * time.Second): t.Fatalf("expected lock timeout after 5 seconds, waited 10 seconds") } if len(input.answers) != 2 { t.Fatalf("expected unused answers, got: %v", input.answers) } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in the remote backend") { t.Fatalf("expected remote backend header in output: %s", output) } if !strings.Contains(output, "Lock timeout exceeded") { t.Fatalf("expected lock timout error in output: %s", output) } if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("unexpected plan summery in output: %s", output) } } func TestRemote_planDestroy(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") defer configCleanup() op.Destroy = true 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") } } func TestRemote_planDestroyNoConfig(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/empty") defer configCleanup() op.Destroy = true 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") } } func TestRemote_planWithWorkingDirectory(t *testing.T) { b := testBackendDefault(t) options := tfe.WorkspaceUpdateOptions{ WorkingDirectory: tfe.String("terraform"), } // Configure the workspace to use a custom working direcrtory. _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.workspace, options) if err != nil { t.Fatalf("error configuring working directory: %v", err) } op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-with-working-directory/terraform") 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() 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 plan 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) } } func TestRemote_planPolicyPass(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-policy-passed") 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() 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 plan 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, "Sentinel Result: true") { t.Fatalf("expected polic check result in output: %s", output) } } func TestRemote_planPolicyHardFail(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-policy-hard-failed") 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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "hard failed") { t.Fatalf("expected a policy check error, got: %v", errOutput) } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan 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, "Sentinel Result: false") { t.Fatalf("expected policy check result in output: %s", output) } } func TestRemote_planPolicySoftFail(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-policy-soft-failed") 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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(errOutput, "soft failed") { t.Fatalf("expected a policy check error, got: %v", errOutput) } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan 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, "Sentinel Result: false") { t.Fatalf("expected policy check result in output: %s", output) } } func TestRemote_planWithRemoteError(t *testing.T) { b := testBackendDefault(t) op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-with-error") 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() if run.Result == backend.OperationSuccess { t.Fatal("expected plan 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, "Running plan in the remote backend") { t.Fatalf("expected remote backend header in output: %s", output) } if !strings.Contains(output, "null_resource.foo: 1 error") { t.Fatalf("expected plan error in output: %s", output) } }