github.com/hugorut/terraform@v1.1.3/src/backend/remote/backend_plan_test.go (about) 1 package remote 2 3 import ( 4 "context" 5 "os" 6 "os/signal" 7 "strings" 8 "syscall" 9 "testing" 10 "time" 11 12 "github.com/google/go-cmp/cmp" 13 tfe "github.com/hashicorp/go-tfe" 14 "github.com/hugorut/terraform/src/addrs" 15 "github.com/hugorut/terraform/src/backend" 16 "github.com/hugorut/terraform/src/cloud" 17 "github.com/hugorut/terraform/src/command/arguments" 18 "github.com/hugorut/terraform/src/command/clistate" 19 "github.com/hugorut/terraform/src/command/views" 20 "github.com/hugorut/terraform/src/depsfile" 21 "github.com/hugorut/terraform/src/initwd" 22 "github.com/hugorut/terraform/src/plans" 23 "github.com/hugorut/terraform/src/plans/planfile" 24 "github.com/hugorut/terraform/src/states/statemgr" 25 "github.com/hugorut/terraform/src/terminal" 26 "github.com/hugorut/terraform/src/terraform" 27 "github.com/mitchellh/cli" 28 ) 29 30 func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 31 t.Helper() 32 33 return testOperationPlanWithTimeout(t, configDir, 0) 34 } 35 36 func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 37 t.Helper() 38 39 _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) 40 41 streams, done := terminal.StreamsForTesting(t) 42 view := views.NewView(streams) 43 stateLockerView := views.NewStateLocker(arguments.ViewHuman, view) 44 operationView := views.NewOperation(arguments.ViewHuman, false, view) 45 46 // Many of our tests use an overridden "null" provider that's just in-memory 47 // inside the test process, not a separate plugin on disk. 48 depLocks := depsfile.NewLocks() 49 depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")) 50 51 return &backend.Operation{ 52 ConfigDir: configDir, 53 ConfigLoader: configLoader, 54 PlanRefresh: true, 55 StateLocker: clistate.NewLocker(timeout, stateLockerView), 56 Type: backend.OperationTypePlan, 57 View: operationView, 58 DependencyLocks: depLocks, 59 }, configCleanup, done 60 } 61 62 func TestRemote_planBasic(t *testing.T) { 63 b, bCleanup := testBackendDefault(t) 64 defer bCleanup() 65 66 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 67 defer configCleanup() 68 defer done(t) 69 70 op.Workspace = backend.DefaultStateName 71 72 run, err := b.Operation(context.Background(), op) 73 if err != nil { 74 t.Fatalf("error starting operation: %v", err) 75 } 76 77 <-run.Done() 78 if run.Result != backend.OperationSuccess { 79 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 80 } 81 if run.PlanEmpty { 82 t.Fatal("expected a non-empty plan") 83 } 84 85 output := b.CLI.(*cli.MockUi).OutputWriter.String() 86 if !strings.Contains(output, "Running plan in the remote backend") { 87 t.Fatalf("expected remote backend header in output: %s", output) 88 } 89 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 90 t.Fatalf("expected plan summary in output: %s", output) 91 } 92 93 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 94 // An error suggests that the state was not unlocked after the operation finished 95 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 96 t.Fatalf("unexpected error locking state after successful plan: %s", err.Error()) 97 } 98 } 99 100 func TestRemote_planCanceled(t *testing.T) { 101 b, bCleanup := testBackendDefault(t) 102 defer bCleanup() 103 104 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 105 defer configCleanup() 106 defer done(t) 107 108 op.Workspace = backend.DefaultStateName 109 110 run, err := b.Operation(context.Background(), op) 111 if err != nil { 112 t.Fatalf("error starting operation: %v", err) 113 } 114 115 // Stop the run to simulate a Ctrl-C. 116 run.Stop() 117 118 <-run.Done() 119 if run.Result == backend.OperationSuccess { 120 t.Fatal("expected plan operation to fail") 121 } 122 123 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 124 // An error suggests that the state was not unlocked after the operation finished 125 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 126 t.Fatalf("unexpected error locking state after cancelled plan: %s", err.Error()) 127 } 128 } 129 130 func TestRemote_planLongLine(t *testing.T) { 131 b, bCleanup := testBackendDefault(t) 132 defer bCleanup() 133 134 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-long-line") 135 defer configCleanup() 136 defer done(t) 137 138 op.Workspace = backend.DefaultStateName 139 140 run, err := b.Operation(context.Background(), op) 141 if err != nil { 142 t.Fatalf("error starting operation: %v", err) 143 } 144 145 <-run.Done() 146 if run.Result != backend.OperationSuccess { 147 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 148 } 149 if run.PlanEmpty { 150 t.Fatal("expected a non-empty plan") 151 } 152 153 output := b.CLI.(*cli.MockUi).OutputWriter.String() 154 if !strings.Contains(output, "Running plan in the remote backend") { 155 t.Fatalf("expected remote backend header in output: %s", output) 156 } 157 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 158 t.Fatalf("expected plan summary in output: %s", output) 159 } 160 } 161 162 func TestRemote_planWithoutPermissions(t *testing.T) { 163 b, bCleanup := testBackendNoDefault(t) 164 defer bCleanup() 165 166 // Create a named workspace without permissions. 167 w, err := b.client.Workspaces.Create( 168 context.Background(), 169 b.organization, 170 tfe.WorkspaceCreateOptions{ 171 Name: tfe.String(b.prefix + "prod"), 172 }, 173 ) 174 if err != nil { 175 t.Fatalf("error creating named workspace: %v", err) 176 } 177 w.Permissions.CanQueueRun = false 178 179 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 180 defer configCleanup() 181 182 op.Workspace = "prod" 183 184 run, err := b.Operation(context.Background(), op) 185 if err != nil { 186 t.Fatalf("error starting operation: %v", err) 187 } 188 189 <-run.Done() 190 output := done(t) 191 if run.Result == backend.OperationSuccess { 192 t.Fatal("expected plan operation to fail") 193 } 194 195 errOutput := output.Stderr() 196 if !strings.Contains(errOutput, "Insufficient rights to generate a plan") { 197 t.Fatalf("expected a permissions error, got: %v", errOutput) 198 } 199 } 200 201 func TestRemote_planWithParallelism(t *testing.T) { 202 b, bCleanup := testBackendDefault(t) 203 defer bCleanup() 204 205 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 206 defer configCleanup() 207 208 if b.ContextOpts == nil { 209 b.ContextOpts = &terraform.ContextOpts{} 210 } 211 b.ContextOpts.Parallelism = 3 212 op.Workspace = backend.DefaultStateName 213 214 run, err := b.Operation(context.Background(), op) 215 if err != nil { 216 t.Fatalf("error starting operation: %v", err) 217 } 218 219 <-run.Done() 220 output := done(t) 221 if run.Result == backend.OperationSuccess { 222 t.Fatal("expected plan operation to fail") 223 } 224 225 errOutput := output.Stderr() 226 if !strings.Contains(errOutput, "parallelism values are currently not supported") { 227 t.Fatalf("expected a parallelism error, got: %v", errOutput) 228 } 229 } 230 231 func TestRemote_planWithPlan(t *testing.T) { 232 b, bCleanup := testBackendDefault(t) 233 defer bCleanup() 234 235 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 236 defer configCleanup() 237 238 op.PlanFile = &planfile.Reader{} 239 op.Workspace = backend.DefaultStateName 240 241 run, err := b.Operation(context.Background(), op) 242 if err != nil { 243 t.Fatalf("error starting operation: %v", err) 244 } 245 246 <-run.Done() 247 output := done(t) 248 if run.Result == backend.OperationSuccess { 249 t.Fatal("expected plan operation to fail") 250 } 251 if !run.PlanEmpty { 252 t.Fatalf("expected plan to be empty") 253 } 254 255 errOutput := output.Stderr() 256 if !strings.Contains(errOutput, "saved plan is currently not supported") { 257 t.Fatalf("expected a saved plan error, got: %v", errOutput) 258 } 259 } 260 261 func TestRemote_planWithPath(t *testing.T) { 262 b, bCleanup := testBackendDefault(t) 263 defer bCleanup() 264 265 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 266 defer configCleanup() 267 268 op.PlanOutPath = "./testdata/plan" 269 op.Workspace = backend.DefaultStateName 270 271 run, err := b.Operation(context.Background(), op) 272 if err != nil { 273 t.Fatalf("error starting operation: %v", err) 274 } 275 276 <-run.Done() 277 output := done(t) 278 if run.Result == backend.OperationSuccess { 279 t.Fatal("expected plan operation to fail") 280 } 281 if !run.PlanEmpty { 282 t.Fatalf("expected plan to be empty") 283 } 284 285 errOutput := output.Stderr() 286 if !strings.Contains(errOutput, "generated plan is currently not supported") { 287 t.Fatalf("expected a generated plan error, got: %v", errOutput) 288 } 289 } 290 291 func TestRemote_planWithoutRefresh(t *testing.T) { 292 b, bCleanup := testBackendDefault(t) 293 defer bCleanup() 294 295 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 296 defer configCleanup() 297 defer done(t) 298 299 op.PlanRefresh = false 300 op.Workspace = backend.DefaultStateName 301 302 run, err := b.Operation(context.Background(), op) 303 if err != nil { 304 t.Fatalf("error starting operation: %v", err) 305 } 306 307 <-run.Done() 308 if run.Result != backend.OperationSuccess { 309 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 310 } 311 if run.PlanEmpty { 312 t.Fatal("expected a non-empty plan") 313 } 314 315 // We should find a run inside the mock client that has refresh set 316 // to false. 317 runsAPI := b.client.Runs.(*cloud.MockRuns) 318 if got, want := len(runsAPI.Runs), 1; got != want { 319 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 320 } 321 for _, run := range runsAPI.Runs { 322 if diff := cmp.Diff(false, run.Refresh); diff != "" { 323 t.Errorf("wrong Refresh setting in the created run\n%s", diff) 324 } 325 } 326 } 327 328 func TestRemote_planWithoutRefreshIncompatibleAPIVersion(t *testing.T) { 329 b, bCleanup := testBackendDefault(t) 330 defer bCleanup() 331 332 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 333 defer configCleanup() 334 335 b.client.SetFakeRemoteAPIVersion("2.3") 336 337 op.PlanRefresh = false 338 op.Workspace = backend.DefaultStateName 339 340 run, err := b.Operation(context.Background(), op) 341 if err != nil { 342 t.Fatalf("error starting operation: %v", err) 343 } 344 345 <-run.Done() 346 output := done(t) 347 if run.Result == backend.OperationSuccess { 348 t.Fatal("expected plan operation to fail") 349 } 350 if !run.PlanEmpty { 351 t.Fatalf("expected plan to be empty") 352 } 353 354 errOutput := output.Stderr() 355 if !strings.Contains(errOutput, "Planning without refresh is not supported") { 356 t.Fatalf("expected not supported error, got: %v", errOutput) 357 } 358 } 359 360 func TestRemote_planWithRefreshOnly(t *testing.T) { 361 b, bCleanup := testBackendDefault(t) 362 defer bCleanup() 363 364 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 365 defer configCleanup() 366 defer done(t) 367 368 op.PlanMode = plans.RefreshOnlyMode 369 op.Workspace = backend.DefaultStateName 370 371 run, err := b.Operation(context.Background(), op) 372 if err != nil { 373 t.Fatalf("error starting operation: %v", err) 374 } 375 376 <-run.Done() 377 if run.Result != backend.OperationSuccess { 378 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 379 } 380 if run.PlanEmpty { 381 t.Fatal("expected a non-empty plan") 382 } 383 384 // We should find a run inside the mock client that has refresh-only set 385 // to true. 386 runsAPI := b.client.Runs.(*cloud.MockRuns) 387 if got, want := len(runsAPI.Runs), 1; got != want { 388 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 389 } 390 for _, run := range runsAPI.Runs { 391 if diff := cmp.Diff(true, run.RefreshOnly); diff != "" { 392 t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff) 393 } 394 } 395 } 396 397 func TestRemote_planWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) { 398 b, bCleanup := testBackendDefault(t) 399 defer bCleanup() 400 401 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 402 defer configCleanup() 403 404 b.client.SetFakeRemoteAPIVersion("2.3") 405 406 op.PlanMode = plans.RefreshOnlyMode 407 op.Workspace = backend.DefaultStateName 408 409 run, err := b.Operation(context.Background(), op) 410 if err != nil { 411 t.Fatalf("error starting operation: %v", err) 412 } 413 414 <-run.Done() 415 output := done(t) 416 if run.Result == backend.OperationSuccess { 417 t.Fatal("expected plan operation to fail") 418 } 419 if !run.PlanEmpty { 420 t.Fatalf("expected plan to be empty") 421 } 422 423 errOutput := output.Stderr() 424 if !strings.Contains(errOutput, "Refresh-only mode is not supported") { 425 t.Fatalf("expected not supported error, got: %v", errOutput) 426 } 427 } 428 429 func TestRemote_planWithTarget(t *testing.T) { 430 b, bCleanup := testBackendDefault(t) 431 defer bCleanup() 432 433 // When the backend code creates a new run, we'll tweak it so that it 434 // has a cost estimation object with the "skipped_due_to_targeting" status, 435 // emulating how a real server is expected to behave in that case. 436 b.client.Runs.(*cloud.MockRuns).ModifyNewRun = func(client *cloud.MockClient, options tfe.RunCreateOptions, run *tfe.Run) { 437 const fakeID = "fake" 438 // This is the cost estimate object embedded in the run itself which 439 // the backend will use to learn the ID to request from the cost 440 // estimates endpoint. It's pending to simulate what a freshly-created 441 // run is likely to look like. 442 run.CostEstimate = &tfe.CostEstimate{ 443 ID: fakeID, 444 Status: "pending", 445 } 446 // The backend will then use the main cost estimation API to retrieve 447 // the same ID indicated in the object above, where we'll then return 448 // the status "skipped_due_to_targeting" to trigger the special skip 449 // message in the backend output. 450 client.CostEstimates.Estimations[fakeID] = &tfe.CostEstimate{ 451 ID: fakeID, 452 Status: "skipped_due_to_targeting", 453 } 454 } 455 456 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 457 defer configCleanup() 458 defer done(t) 459 460 addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") 461 462 op.Targets = []addrs.Targetable{addr} 463 op.Workspace = backend.DefaultStateName 464 465 run, err := b.Operation(context.Background(), op) 466 if err != nil { 467 t.Fatalf("error starting operation: %v", err) 468 } 469 470 <-run.Done() 471 if run.Result != backend.OperationSuccess { 472 t.Fatal("expected plan operation to succeed") 473 } 474 if run.PlanEmpty { 475 t.Fatalf("expected plan to be non-empty") 476 } 477 478 // testBackendDefault above attached a "mock UI" to our backend, so we 479 // can retrieve its non-error output via the OutputWriter in-memory buffer. 480 gotOutput := b.CLI.(*cli.MockUi).OutputWriter.String() 481 if wantOutput := "Not available for this plan, because it was created with the -target option."; !strings.Contains(gotOutput, wantOutput) { 482 t.Errorf("missing message about skipped cost estimation\ngot:\n%s\nwant substring: %s", gotOutput, wantOutput) 483 } 484 485 // We should find a run inside the mock client that has the same 486 // target address we requested above. 487 runsAPI := b.client.Runs.(*cloud.MockRuns) 488 if got, want := len(runsAPI.Runs), 1; got != want { 489 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 490 } 491 for _, run := range runsAPI.Runs { 492 if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { 493 t.Errorf("wrong TargetAddrs in the created run\n%s", diff) 494 } 495 } 496 } 497 498 func TestRemote_planWithTargetIncompatibleAPIVersion(t *testing.T) { 499 b, bCleanup := testBackendDefault(t) 500 defer bCleanup() 501 502 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 503 defer configCleanup() 504 505 // Set the tfe client's RemoteAPIVersion to an empty string, to mimic 506 // API versions prior to 2.3. 507 b.client.SetFakeRemoteAPIVersion("") 508 509 addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") 510 511 op.Targets = []addrs.Targetable{addr} 512 op.Workspace = backend.DefaultStateName 513 514 run, err := b.Operation(context.Background(), op) 515 if err != nil { 516 t.Fatalf("error starting operation: %v", err) 517 } 518 519 <-run.Done() 520 output := done(t) 521 if run.Result == backend.OperationSuccess { 522 t.Fatal("expected plan operation to fail") 523 } 524 if !run.PlanEmpty { 525 t.Fatalf("expected plan to be empty") 526 } 527 528 errOutput := output.Stderr() 529 if !strings.Contains(errOutput, "Resource targeting is not supported") { 530 t.Fatalf("expected a targeting error, got: %v", errOutput) 531 } 532 } 533 534 func TestRemote_planWithReplace(t *testing.T) { 535 b, bCleanup := testBackendDefault(t) 536 defer bCleanup() 537 538 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 539 defer configCleanup() 540 defer done(t) 541 542 addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") 543 544 op.ForceReplace = []addrs.AbsResourceInstance{addr} 545 op.Workspace = backend.DefaultStateName 546 547 run, err := b.Operation(context.Background(), op) 548 if err != nil { 549 t.Fatalf("error starting operation: %v", err) 550 } 551 552 <-run.Done() 553 if run.Result != backend.OperationSuccess { 554 t.Fatal("expected plan operation to succeed") 555 } 556 if run.PlanEmpty { 557 t.Fatalf("expected plan to be non-empty") 558 } 559 560 // We should find a run inside the mock client that has the same 561 // refresh address we requested above. 562 runsAPI := b.client.Runs.(*cloud.MockRuns) 563 if got, want := len(runsAPI.Runs), 1; got != want { 564 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 565 } 566 for _, run := range runsAPI.Runs { 567 if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" { 568 t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff) 569 } 570 } 571 } 572 573 func TestRemote_planWithReplaceIncompatibleAPIVersion(t *testing.T) { 574 b, bCleanup := testBackendDefault(t) 575 defer bCleanup() 576 577 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 578 defer configCleanup() 579 580 b.client.SetFakeRemoteAPIVersion("2.3") 581 582 addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") 583 584 op.ForceReplace = []addrs.AbsResourceInstance{addr} 585 op.Workspace = backend.DefaultStateName 586 587 run, err := b.Operation(context.Background(), op) 588 if err != nil { 589 t.Fatalf("error starting operation: %v", err) 590 } 591 592 <-run.Done() 593 output := done(t) 594 if run.Result == backend.OperationSuccess { 595 t.Fatal("expected plan operation to fail") 596 } 597 if !run.PlanEmpty { 598 t.Fatalf("expected plan to be empty") 599 } 600 601 errOutput := output.Stderr() 602 if !strings.Contains(errOutput, "Planning resource replacements is not supported") { 603 t.Fatalf("expected not supported error, got: %v", errOutput) 604 } 605 } 606 607 func TestRemote_planWithVariables(t *testing.T) { 608 b, bCleanup := testBackendDefault(t) 609 defer bCleanup() 610 611 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-variables") 612 defer configCleanup() 613 614 op.Variables = testVariables(terraform.ValueFromCLIArg, "foo", "bar") 615 op.Workspace = backend.DefaultStateName 616 617 run, err := b.Operation(context.Background(), op) 618 if err != nil { 619 t.Fatalf("error starting operation: %v", err) 620 } 621 622 <-run.Done() 623 output := done(t) 624 if run.Result == backend.OperationSuccess { 625 t.Fatal("expected plan operation to fail") 626 } 627 628 errOutput := output.Stderr() 629 if !strings.Contains(errOutput, "variables are currently not supported") { 630 t.Fatalf("expected a variables error, got: %v", errOutput) 631 } 632 } 633 634 func TestRemote_planNoConfig(t *testing.T) { 635 b, bCleanup := testBackendDefault(t) 636 defer bCleanup() 637 638 op, configCleanup, done := testOperationPlan(t, "./testdata/empty") 639 defer configCleanup() 640 641 op.Workspace = backend.DefaultStateName 642 643 run, err := b.Operation(context.Background(), op) 644 if err != nil { 645 t.Fatalf("error starting operation: %v", err) 646 } 647 648 <-run.Done() 649 output := done(t) 650 if run.Result == backend.OperationSuccess { 651 t.Fatal("expected plan operation to fail") 652 } 653 if !run.PlanEmpty { 654 t.Fatalf("expected plan to be empty") 655 } 656 657 errOutput := output.Stderr() 658 if !strings.Contains(errOutput, "configuration files found") { 659 t.Fatalf("expected configuration files error, got: %v", errOutput) 660 } 661 } 662 663 func TestRemote_planNoChanges(t *testing.T) { 664 b, bCleanup := testBackendDefault(t) 665 defer bCleanup() 666 667 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-no-changes") 668 defer configCleanup() 669 defer done(t) 670 671 op.Workspace = backend.DefaultStateName 672 673 run, err := b.Operation(context.Background(), op) 674 if err != nil { 675 t.Fatalf("error starting operation: %v", err) 676 } 677 678 <-run.Done() 679 if run.Result != backend.OperationSuccess { 680 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 681 } 682 if !run.PlanEmpty { 683 t.Fatalf("expected plan to be empty") 684 } 685 686 output := b.CLI.(*cli.MockUi).OutputWriter.String() 687 if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") { 688 t.Fatalf("expected no changes in plan summary: %s", output) 689 } 690 if !strings.Contains(output, "Sentinel Result: true") { 691 t.Fatalf("expected policy check result in output: %s", output) 692 } 693 } 694 695 func TestRemote_planForceLocal(t *testing.T) { 696 // Set TF_FORCE_LOCAL_BACKEND so the remote backend will use 697 // the local backend with itself as embedded backend. 698 if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil { 699 t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err) 700 } 701 defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND") 702 703 b, bCleanup := testBackendDefault(t) 704 defer bCleanup() 705 706 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 707 defer configCleanup() 708 defer done(t) 709 710 op.Workspace = backend.DefaultStateName 711 712 streams, done := terminal.StreamsForTesting(t) 713 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 714 op.View = view 715 716 run, err := b.Operation(context.Background(), op) 717 if err != nil { 718 t.Fatalf("error starting operation: %v", err) 719 } 720 721 <-run.Done() 722 if run.Result != backend.OperationSuccess { 723 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 724 } 725 if run.PlanEmpty { 726 t.Fatalf("expected a non-empty plan") 727 } 728 729 output := b.CLI.(*cli.MockUi).OutputWriter.String() 730 if strings.Contains(output, "Running plan in the remote backend") { 731 t.Fatalf("unexpected remote backend header in output: %s", output) 732 } 733 if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 734 t.Fatalf("expected plan summary in output: %s", output) 735 } 736 } 737 738 func TestRemote_planWithoutOperationsEntitlement(t *testing.T) { 739 b, bCleanup := testBackendNoOperations(t) 740 defer bCleanup() 741 742 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 743 defer configCleanup() 744 defer done(t) 745 746 op.Workspace = backend.DefaultStateName 747 748 streams, done := terminal.StreamsForTesting(t) 749 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 750 op.View = view 751 752 run, err := b.Operation(context.Background(), op) 753 if err != nil { 754 t.Fatalf("error starting operation: %v", err) 755 } 756 757 <-run.Done() 758 if run.Result != backend.OperationSuccess { 759 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 760 } 761 if run.PlanEmpty { 762 t.Fatalf("expected a non-empty plan") 763 } 764 765 output := b.CLI.(*cli.MockUi).OutputWriter.String() 766 if strings.Contains(output, "Running plan in the remote backend") { 767 t.Fatalf("unexpected remote backend header in output: %s", output) 768 } 769 if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 770 t.Fatalf("expected plan summary in output: %s", output) 771 } 772 } 773 774 func TestRemote_planWorkspaceWithoutOperations(t *testing.T) { 775 b, bCleanup := testBackendNoDefault(t) 776 defer bCleanup() 777 778 ctx := context.Background() 779 780 // Create a named workspace that doesn't allow operations. 781 _, err := b.client.Workspaces.Create( 782 ctx, 783 b.organization, 784 tfe.WorkspaceCreateOptions{ 785 Name: tfe.String(b.prefix + "no-operations"), 786 }, 787 ) 788 if err != nil { 789 t.Fatalf("error creating named workspace: %v", err) 790 } 791 792 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 793 defer configCleanup() 794 defer done(t) 795 796 op.Workspace = "no-operations" 797 798 streams, done := terminal.StreamsForTesting(t) 799 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 800 op.View = view 801 802 run, err := b.Operation(ctx, op) 803 if err != nil { 804 t.Fatalf("error starting operation: %v", err) 805 } 806 807 <-run.Done() 808 if run.Result != backend.OperationSuccess { 809 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 810 } 811 if run.PlanEmpty { 812 t.Fatalf("expected a non-empty plan") 813 } 814 815 output := b.CLI.(*cli.MockUi).OutputWriter.String() 816 if strings.Contains(output, "Running plan in the remote backend") { 817 t.Fatalf("unexpected remote backend header in output: %s", output) 818 } 819 if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 820 t.Fatalf("expected plan summary in output: %s", output) 821 } 822 } 823 824 func TestRemote_planLockTimeout(t *testing.T) { 825 b, bCleanup := testBackendDefault(t) 826 defer bCleanup() 827 828 ctx := context.Background() 829 830 // Retrieve the workspace used to run this operation in. 831 w, err := b.client.Workspaces.Read(ctx, b.organization, b.workspace) 832 if err != nil { 833 t.Fatalf("error retrieving workspace: %v", err) 834 } 835 836 // Create a new configuration version. 837 c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{}) 838 if err != nil { 839 t.Fatalf("error creating configuration version: %v", err) 840 } 841 842 // Create a pending run to block this run. 843 _, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{ 844 ConfigurationVersion: c, 845 Workspace: w, 846 }) 847 if err != nil { 848 t.Fatalf("error creating pending run: %v", err) 849 } 850 851 op, configCleanup, done := testOperationPlanWithTimeout(t, "./testdata/plan", 50) 852 defer configCleanup() 853 defer done(t) 854 855 input := testInput(t, map[string]string{ 856 "cancel": "yes", 857 "approve": "yes", 858 }) 859 860 op.UIIn = input 861 op.UIOut = b.CLI 862 op.Workspace = backend.DefaultStateName 863 864 _, err = b.Operation(context.Background(), op) 865 if err != nil { 866 t.Fatalf("error starting operation: %v", err) 867 } 868 869 sigint := make(chan os.Signal, 1) 870 signal.Notify(sigint, syscall.SIGINT) 871 select { 872 case <-sigint: 873 // Stop redirecting SIGINT signals. 874 signal.Stop(sigint) 875 case <-time.After(200 * time.Millisecond): 876 t.Fatalf("expected lock timeout after 50 milliseconds, waited 200 milliseconds") 877 } 878 879 if len(input.answers) != 2 { 880 t.Fatalf("expected unused answers, got: %v", input.answers) 881 } 882 883 output := b.CLI.(*cli.MockUi).OutputWriter.String() 884 if !strings.Contains(output, "Running plan in the remote backend") { 885 t.Fatalf("expected remote backend header in output: %s", output) 886 } 887 if !strings.Contains(output, "Lock timeout exceeded") { 888 t.Fatalf("expected lock timout error in output: %s", output) 889 } 890 if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 891 t.Fatalf("unexpected plan summary in output: %s", output) 892 } 893 } 894 895 func TestRemote_planDestroy(t *testing.T) { 896 b, bCleanup := testBackendDefault(t) 897 defer bCleanup() 898 899 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 900 defer configCleanup() 901 defer done(t) 902 903 op.PlanMode = plans.DestroyMode 904 op.Workspace = backend.DefaultStateName 905 906 run, err := b.Operation(context.Background(), op) 907 if err != nil { 908 t.Fatalf("error starting operation: %v", err) 909 } 910 911 <-run.Done() 912 if run.Result != backend.OperationSuccess { 913 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 914 } 915 if run.PlanEmpty { 916 t.Fatalf("expected a non-empty plan") 917 } 918 } 919 920 func TestRemote_planDestroyNoConfig(t *testing.T) { 921 b, bCleanup := testBackendDefault(t) 922 defer bCleanup() 923 924 op, configCleanup, done := testOperationPlan(t, "./testdata/empty") 925 defer configCleanup() 926 defer done(t) 927 928 op.PlanMode = plans.DestroyMode 929 op.Workspace = backend.DefaultStateName 930 931 run, err := b.Operation(context.Background(), op) 932 if err != nil { 933 t.Fatalf("error starting operation: %v", err) 934 } 935 936 <-run.Done() 937 if run.Result != backend.OperationSuccess { 938 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 939 } 940 if run.PlanEmpty { 941 t.Fatalf("expected a non-empty plan") 942 } 943 } 944 945 func TestRemote_planWithWorkingDirectory(t *testing.T) { 946 b, bCleanup := testBackendDefault(t) 947 defer bCleanup() 948 949 options := tfe.WorkspaceUpdateOptions{ 950 WorkingDirectory: tfe.String("terraform"), 951 } 952 953 // Configure the workspace to use a custom working directory. 954 _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.workspace, options) 955 if err != nil { 956 t.Fatalf("error configuring working directory: %v", err) 957 } 958 959 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-working-directory/terraform") 960 defer configCleanup() 961 defer done(t) 962 963 op.Workspace = backend.DefaultStateName 964 965 run, err := b.Operation(context.Background(), op) 966 if err != nil { 967 t.Fatalf("error starting operation: %v", err) 968 } 969 970 <-run.Done() 971 if run.Result != backend.OperationSuccess { 972 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 973 } 974 if run.PlanEmpty { 975 t.Fatalf("expected a non-empty plan") 976 } 977 978 output := b.CLI.(*cli.MockUi).OutputWriter.String() 979 if !strings.Contains(output, "The remote workspace is configured to work with configuration") { 980 t.Fatalf("expected working directory warning: %s", output) 981 } 982 if !strings.Contains(output, "Running plan in the remote backend") { 983 t.Fatalf("expected remote backend header in output: %s", output) 984 } 985 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 986 t.Fatalf("expected plan summary in output: %s", output) 987 } 988 } 989 990 func TestRemote_planWithWorkingDirectoryFromCurrentPath(t *testing.T) { 991 b, bCleanup := testBackendDefault(t) 992 defer bCleanup() 993 994 options := tfe.WorkspaceUpdateOptions{ 995 WorkingDirectory: tfe.String("terraform"), 996 } 997 998 // Configure the workspace to use a custom working directory. 999 _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.workspace, options) 1000 if err != nil { 1001 t.Fatalf("error configuring working directory: %v", err) 1002 } 1003 1004 wd, err := os.Getwd() 1005 if err != nil { 1006 t.Fatalf("error getting current working directory: %v", err) 1007 } 1008 1009 // We need to change into the configuration directory to make sure 1010 // the logic to upload the correct slug is working as expected. 1011 if err := os.Chdir("./testdata/plan-with-working-directory/terraform"); err != nil { 1012 t.Fatalf("error changing directory: %v", err) 1013 } 1014 defer os.Chdir(wd) // Make sure we change back again when were done. 1015 1016 // For this test we need to give our current directory instead of the 1017 // full path to the configuration as we already changed directories. 1018 op, configCleanup, done := testOperationPlan(t, ".") 1019 defer configCleanup() 1020 defer done(t) 1021 1022 op.Workspace = backend.DefaultStateName 1023 1024 run, err := b.Operation(context.Background(), op) 1025 if err != nil { 1026 t.Fatalf("error starting operation: %v", err) 1027 } 1028 1029 <-run.Done() 1030 if run.Result != backend.OperationSuccess { 1031 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1032 } 1033 if run.PlanEmpty { 1034 t.Fatalf("expected a non-empty plan") 1035 } 1036 1037 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1038 if !strings.Contains(output, "Running plan in the remote backend") { 1039 t.Fatalf("expected remote backend header in output: %s", output) 1040 } 1041 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1042 t.Fatalf("expected plan summary in output: %s", output) 1043 } 1044 } 1045 1046 func TestRemote_planCostEstimation(t *testing.T) { 1047 b, bCleanup := testBackendDefault(t) 1048 defer bCleanup() 1049 1050 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cost-estimation") 1051 defer configCleanup() 1052 defer done(t) 1053 1054 op.Workspace = backend.DefaultStateName 1055 1056 run, err := b.Operation(context.Background(), op) 1057 if err != nil { 1058 t.Fatalf("error starting operation: %v", err) 1059 } 1060 1061 <-run.Done() 1062 if run.Result != backend.OperationSuccess { 1063 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1064 } 1065 if run.PlanEmpty { 1066 t.Fatalf("expected a non-empty plan") 1067 } 1068 1069 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1070 if !strings.Contains(output, "Running plan in the remote backend") { 1071 t.Fatalf("expected remote backend header in output: %s", output) 1072 } 1073 if !strings.Contains(output, "Resources: 1 of 1 estimated") { 1074 t.Fatalf("expected cost estimate result in output: %s", output) 1075 } 1076 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1077 t.Fatalf("expected plan summary in output: %s", output) 1078 } 1079 } 1080 1081 func TestRemote_planPolicyPass(t *testing.T) { 1082 b, bCleanup := testBackendDefault(t) 1083 defer bCleanup() 1084 1085 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-passed") 1086 defer configCleanup() 1087 defer done(t) 1088 1089 op.Workspace = backend.DefaultStateName 1090 1091 run, err := b.Operation(context.Background(), op) 1092 if err != nil { 1093 t.Fatalf("error starting operation: %v", err) 1094 } 1095 1096 <-run.Done() 1097 if run.Result != backend.OperationSuccess { 1098 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1099 } 1100 if run.PlanEmpty { 1101 t.Fatalf("expected a non-empty plan") 1102 } 1103 1104 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1105 if !strings.Contains(output, "Running plan in the remote backend") { 1106 t.Fatalf("expected remote backend header in output: %s", output) 1107 } 1108 if !strings.Contains(output, "Sentinel Result: true") { 1109 t.Fatalf("expected policy check result in output: %s", output) 1110 } 1111 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1112 t.Fatalf("expected plan summary in output: %s", output) 1113 } 1114 } 1115 1116 func TestRemote_planPolicyHardFail(t *testing.T) { 1117 b, bCleanup := testBackendDefault(t) 1118 defer bCleanup() 1119 1120 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-hard-failed") 1121 defer configCleanup() 1122 1123 op.Workspace = backend.DefaultStateName 1124 1125 run, err := b.Operation(context.Background(), op) 1126 if err != nil { 1127 t.Fatalf("error starting operation: %v", err) 1128 } 1129 1130 <-run.Done() 1131 viewOutput := done(t) 1132 if run.Result == backend.OperationSuccess { 1133 t.Fatal("expected plan operation to fail") 1134 } 1135 if !run.PlanEmpty { 1136 t.Fatalf("expected plan to be empty") 1137 } 1138 1139 errOutput := viewOutput.Stderr() 1140 if !strings.Contains(errOutput, "hard failed") { 1141 t.Fatalf("expected a policy check error, got: %v", errOutput) 1142 } 1143 1144 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1145 if !strings.Contains(output, "Running plan in the remote backend") { 1146 t.Fatalf("expected remote backend header in output: %s", output) 1147 } 1148 if !strings.Contains(output, "Sentinel Result: false") { 1149 t.Fatalf("expected policy check result in output: %s", output) 1150 } 1151 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1152 t.Fatalf("expected plan summary in output: %s", output) 1153 } 1154 } 1155 1156 func TestRemote_planPolicySoftFail(t *testing.T) { 1157 b, bCleanup := testBackendDefault(t) 1158 defer bCleanup() 1159 1160 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-soft-failed") 1161 defer configCleanup() 1162 1163 op.Workspace = backend.DefaultStateName 1164 1165 run, err := b.Operation(context.Background(), op) 1166 if err != nil { 1167 t.Fatalf("error starting operation: %v", err) 1168 } 1169 1170 <-run.Done() 1171 viewOutput := done(t) 1172 if run.Result == backend.OperationSuccess { 1173 t.Fatal("expected plan operation to fail") 1174 } 1175 if !run.PlanEmpty { 1176 t.Fatalf("expected plan to be empty") 1177 } 1178 1179 errOutput := viewOutput.Stderr() 1180 if !strings.Contains(errOutput, "soft failed") { 1181 t.Fatalf("expected a policy check error, got: %v", errOutput) 1182 } 1183 1184 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1185 if !strings.Contains(output, "Running plan in the remote backend") { 1186 t.Fatalf("expected remote backend header in output: %s", output) 1187 } 1188 if !strings.Contains(output, "Sentinel Result: false") { 1189 t.Fatalf("expected policy check result in output: %s", output) 1190 } 1191 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1192 t.Fatalf("expected plan summary in output: %s", output) 1193 } 1194 } 1195 1196 func TestRemote_planWithRemoteError(t *testing.T) { 1197 b, bCleanup := testBackendDefault(t) 1198 defer bCleanup() 1199 1200 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-error") 1201 defer configCleanup() 1202 defer done(t) 1203 1204 op.Workspace = backend.DefaultStateName 1205 1206 run, err := b.Operation(context.Background(), op) 1207 if err != nil { 1208 t.Fatalf("error starting operation: %v", err) 1209 } 1210 1211 <-run.Done() 1212 if run.Result == backend.OperationSuccess { 1213 t.Fatal("expected plan operation to fail") 1214 } 1215 if run.Result.ExitStatus() != 1 { 1216 t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) 1217 } 1218 1219 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1220 if !strings.Contains(output, "Running plan in the remote backend") { 1221 t.Fatalf("expected remote backend header in output: %s", output) 1222 } 1223 if !strings.Contains(output, "null_resource.foo: 1 error") { 1224 t.Fatalf("expected plan error in output: %s", output) 1225 } 1226 } 1227 1228 func TestRemote_planOtherError(t *testing.T) { 1229 b, bCleanup := testBackendDefault(t) 1230 defer bCleanup() 1231 1232 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 1233 defer configCleanup() 1234 defer done(t) 1235 1236 op.Workspace = "network-error" // custom error response in backend_mock.go 1237 1238 _, err := b.Operation(context.Background(), op) 1239 if err == nil { 1240 t.Errorf("expected error, got success") 1241 } 1242 1243 if !strings.Contains(err.Error(), 1244 "The configured \"remote\" backend encountered an unexpected error:\n\nI'm a little teacup") { 1245 t.Fatalf("expected error message, got: %s", err.Error()) 1246 } 1247 }