github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/backend/remote/backend_apply_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 version "github.com/hashicorp/go-version" 15 "github.com/hashicorp/terraform/internal/addrs" 16 "github.com/hashicorp/terraform/internal/backend" 17 "github.com/hashicorp/terraform/internal/command/arguments" 18 "github.com/hashicorp/terraform/internal/command/clistate" 19 "github.com/hashicorp/terraform/internal/command/views" 20 "github.com/hashicorp/terraform/internal/initwd" 21 "github.com/hashicorp/terraform/internal/plans" 22 "github.com/hashicorp/terraform/internal/plans/planfile" 23 "github.com/hashicorp/terraform/internal/states/statemgr" 24 "github.com/hashicorp/terraform/internal/terminal" 25 "github.com/hashicorp/terraform/internal/terraform" 26 tfversion "github.com/hashicorp/terraform/version" 27 "github.com/mitchellh/cli" 28 ) 29 30 func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 31 t.Helper() 32 33 return testOperationApplyWithTimeout(t, configDir, 0) 34 } 35 36 func testOperationApplyWithTimeout(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 return &backend.Operation{ 47 ConfigDir: configDir, 48 ConfigLoader: configLoader, 49 Parallelism: defaultParallelism, 50 PlanRefresh: true, 51 StateLocker: clistate.NewLocker(timeout, stateLockerView), 52 Type: backend.OperationTypeApply, 53 View: operationView, 54 }, configCleanup, done 55 } 56 57 func TestRemote_applyBasic(t *testing.T) { 58 b, bCleanup := testBackendDefault(t) 59 defer bCleanup() 60 61 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 62 defer configCleanup() 63 defer done(t) 64 65 input := testInput(t, map[string]string{ 66 "approve": "yes", 67 }) 68 69 op.UIIn = input 70 op.UIOut = b.CLI 71 op.Workspace = backend.DefaultStateName 72 73 run, err := b.Operation(context.Background(), op) 74 if err != nil { 75 t.Fatalf("error starting operation: %v", err) 76 } 77 78 <-run.Done() 79 if run.Result != backend.OperationSuccess { 80 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 81 } 82 if run.PlanEmpty { 83 t.Fatalf("expected a non-empty plan") 84 } 85 86 if len(input.answers) > 0 { 87 t.Fatalf("expected no unused answers, got: %v", input.answers) 88 } 89 90 output := b.CLI.(*cli.MockUi).OutputWriter.String() 91 if !strings.Contains(output, "Running apply in the remote backend") { 92 t.Fatalf("expected remote backend header in output: %s", output) 93 } 94 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 95 t.Fatalf("expected plan summery in output: %s", output) 96 } 97 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 98 t.Fatalf("expected apply summery in output: %s", output) 99 } 100 101 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 102 // An error suggests that the state was not unlocked after apply 103 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 104 t.Fatalf("unexpected error locking state after apply: %s", err.Error()) 105 } 106 } 107 108 func TestRemote_applyCanceled(t *testing.T) { 109 b, bCleanup := testBackendDefault(t) 110 defer bCleanup() 111 112 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 113 defer configCleanup() 114 defer done(t) 115 116 op.Workspace = backend.DefaultStateName 117 118 run, err := b.Operation(context.Background(), op) 119 if err != nil { 120 t.Fatalf("error starting operation: %v", err) 121 } 122 123 // Stop the run to simulate a Ctrl-C. 124 run.Stop() 125 126 <-run.Done() 127 if run.Result == backend.OperationSuccess { 128 t.Fatal("expected apply operation to fail") 129 } 130 131 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 132 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 133 t.Fatalf("unexpected error locking state after cancelling apply: %s", err.Error()) 134 } 135 } 136 137 func TestRemote_applyWithoutPermissions(t *testing.T) { 138 b, bCleanup := testBackendNoDefault(t) 139 defer bCleanup() 140 141 // Create a named workspace without permissions. 142 w, err := b.client.Workspaces.Create( 143 context.Background(), 144 b.organization, 145 tfe.WorkspaceCreateOptions{ 146 Name: tfe.String(b.prefix + "prod"), 147 }, 148 ) 149 if err != nil { 150 t.Fatalf("error creating named workspace: %v", err) 151 } 152 w.Permissions.CanQueueApply = false 153 154 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 155 defer configCleanup() 156 157 op.UIOut = b.CLI 158 op.Workspace = "prod" 159 160 run, err := b.Operation(context.Background(), op) 161 if err != nil { 162 t.Fatalf("error starting operation: %v", err) 163 } 164 165 <-run.Done() 166 output := done(t) 167 if run.Result == backend.OperationSuccess { 168 t.Fatal("expected apply operation to fail") 169 } 170 171 errOutput := output.Stderr() 172 if !strings.Contains(errOutput, "Insufficient rights to apply changes") { 173 t.Fatalf("expected a permissions error, got: %v", errOutput) 174 } 175 } 176 177 func TestRemote_applyWithVCS(t *testing.T) { 178 b, bCleanup := testBackendNoDefault(t) 179 defer bCleanup() 180 181 // Create a named workspace with a VCS. 182 _, err := b.client.Workspaces.Create( 183 context.Background(), 184 b.organization, 185 tfe.WorkspaceCreateOptions{ 186 Name: tfe.String(b.prefix + "prod"), 187 VCSRepo: &tfe.VCSRepoOptions{}, 188 }, 189 ) 190 if err != nil { 191 t.Fatalf("error creating named workspace: %v", err) 192 } 193 194 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 195 defer configCleanup() 196 197 op.Workspace = "prod" 198 199 run, err := b.Operation(context.Background(), op) 200 if err != nil { 201 t.Fatalf("error starting operation: %v", err) 202 } 203 204 <-run.Done() 205 output := done(t) 206 if run.Result == backend.OperationSuccess { 207 t.Fatal("expected apply operation to fail") 208 } 209 if !run.PlanEmpty { 210 t.Fatalf("expected plan to be empty") 211 } 212 213 errOutput := output.Stderr() 214 if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") { 215 t.Fatalf("expected a VCS error, got: %v", errOutput) 216 } 217 } 218 219 func TestRemote_applyWithParallelism(t *testing.T) { 220 b, bCleanup := testBackendDefault(t) 221 defer bCleanup() 222 223 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 224 defer configCleanup() 225 226 op.Parallelism = 3 227 op.Workspace = backend.DefaultStateName 228 229 run, err := b.Operation(context.Background(), op) 230 if err != nil { 231 t.Fatalf("error starting operation: %v", err) 232 } 233 234 <-run.Done() 235 output := done(t) 236 if run.Result == backend.OperationSuccess { 237 t.Fatal("expected apply operation to fail") 238 } 239 240 errOutput := output.Stderr() 241 if !strings.Contains(errOutput, "parallelism values are currently not supported") { 242 t.Fatalf("expected a parallelism error, got: %v", errOutput) 243 } 244 } 245 246 func TestRemote_applyWithPlan(t *testing.T) { 247 b, bCleanup := testBackendDefault(t) 248 defer bCleanup() 249 250 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 251 defer configCleanup() 252 253 op.PlanFile = &planfile.Reader{} 254 op.Workspace = backend.DefaultStateName 255 256 run, err := b.Operation(context.Background(), op) 257 if err != nil { 258 t.Fatalf("error starting operation: %v", err) 259 } 260 261 <-run.Done() 262 output := done(t) 263 if run.Result == backend.OperationSuccess { 264 t.Fatal("expected apply operation to fail") 265 } 266 if !run.PlanEmpty { 267 t.Fatalf("expected plan to be empty") 268 } 269 270 errOutput := output.Stderr() 271 if !strings.Contains(errOutput, "saved plan is currently not supported") { 272 t.Fatalf("expected a saved plan error, got: %v", errOutput) 273 } 274 } 275 276 func TestRemote_applyWithoutRefresh(t *testing.T) { 277 b, bCleanup := testBackendDefault(t) 278 defer bCleanup() 279 280 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 281 defer configCleanup() 282 defer done(t) 283 284 op.PlanRefresh = false 285 op.Workspace = backend.DefaultStateName 286 287 run, err := b.Operation(context.Background(), op) 288 if err != nil { 289 t.Fatalf("error starting operation: %v", err) 290 } 291 292 <-run.Done() 293 if run.Result != backend.OperationSuccess { 294 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 295 } 296 if run.PlanEmpty { 297 t.Fatalf("expected plan to be non-empty") 298 } 299 300 // We should find a run inside the mock client that has refresh set 301 // to false. 302 runsAPI := b.client.Runs.(*mockRuns) 303 if got, want := len(runsAPI.runs), 1; got != want { 304 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 305 } 306 for _, run := range runsAPI.runs { 307 if diff := cmp.Diff(false, run.Refresh); diff != "" { 308 t.Errorf("wrong Refresh setting in the created run\n%s", diff) 309 } 310 } 311 } 312 313 func TestRemote_applyWithoutRefreshIncompatibleAPIVersion(t *testing.T) { 314 b, bCleanup := testBackendDefault(t) 315 defer bCleanup() 316 317 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 318 defer configCleanup() 319 320 b.client.SetFakeRemoteAPIVersion("2.3") 321 322 op.PlanRefresh = false 323 op.Workspace = backend.DefaultStateName 324 325 run, err := b.Operation(context.Background(), op) 326 if err != nil { 327 t.Fatalf("error starting operation: %v", err) 328 } 329 330 <-run.Done() 331 output := done(t) 332 if run.Result == backend.OperationSuccess { 333 t.Fatal("expected apply operation to fail") 334 } 335 if !run.PlanEmpty { 336 t.Fatalf("expected plan to be empty") 337 } 338 339 errOutput := output.Stderr() 340 if !strings.Contains(errOutput, "Planning without refresh is not supported") { 341 t.Fatalf("expected a not supported error, got: %v", errOutput) 342 } 343 } 344 345 func TestRemote_applyWithRefreshOnly(t *testing.T) { 346 b, bCleanup := testBackendDefault(t) 347 defer bCleanup() 348 349 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 350 defer configCleanup() 351 defer done(t) 352 353 op.PlanMode = plans.RefreshOnlyMode 354 op.Workspace = backend.DefaultStateName 355 356 run, err := b.Operation(context.Background(), op) 357 if err != nil { 358 t.Fatalf("error starting operation: %v", err) 359 } 360 361 <-run.Done() 362 if run.Result != backend.OperationSuccess { 363 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 364 } 365 if run.PlanEmpty { 366 t.Fatalf("expected plan to be non-empty") 367 } 368 369 // We should find a run inside the mock client that has refresh-only set 370 // to true. 371 runsAPI := b.client.Runs.(*mockRuns) 372 if got, want := len(runsAPI.runs), 1; got != want { 373 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 374 } 375 for _, run := range runsAPI.runs { 376 if diff := cmp.Diff(true, run.RefreshOnly); diff != "" { 377 t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff) 378 } 379 } 380 } 381 382 func TestRemote_applyWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) { 383 b, bCleanup := testBackendDefault(t) 384 defer bCleanup() 385 386 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 387 defer configCleanup() 388 389 b.client.SetFakeRemoteAPIVersion("2.3") 390 391 op.PlanMode = plans.RefreshOnlyMode 392 op.Workspace = backend.DefaultStateName 393 394 run, err := b.Operation(context.Background(), op) 395 if err != nil { 396 t.Fatalf("error starting operation: %v", err) 397 } 398 399 <-run.Done() 400 output := done(t) 401 if run.Result == backend.OperationSuccess { 402 t.Fatal("expected apply operation to fail") 403 } 404 if !run.PlanEmpty { 405 t.Fatalf("expected plan to be empty") 406 } 407 408 errOutput := output.Stderr() 409 if !strings.Contains(errOutput, "Refresh-only mode is not supported") { 410 t.Fatalf("expected a not supported error, got: %v", errOutput) 411 } 412 } 413 414 func TestRemote_applyWithTarget(t *testing.T) { 415 b, bCleanup := testBackendDefault(t) 416 defer bCleanup() 417 418 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 419 defer configCleanup() 420 defer done(t) 421 422 addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") 423 424 op.Targets = []addrs.Targetable{addr} 425 op.Workspace = backend.DefaultStateName 426 427 run, err := b.Operation(context.Background(), op) 428 if err != nil { 429 t.Fatalf("error starting operation: %v", err) 430 } 431 432 <-run.Done() 433 if run.Result != backend.OperationSuccess { 434 t.Fatal("expected apply operation to succeed") 435 } 436 if run.PlanEmpty { 437 t.Fatalf("expected plan to be non-empty") 438 } 439 440 // We should find a run inside the mock client that has the same 441 // target address we requested above. 442 runsAPI := b.client.Runs.(*mockRuns) 443 if got, want := len(runsAPI.runs), 1; got != want { 444 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 445 } 446 for _, run := range runsAPI.runs { 447 if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { 448 t.Errorf("wrong TargetAddrs in the created run\n%s", diff) 449 } 450 } 451 } 452 453 func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) { 454 b, bCleanup := testBackendDefault(t) 455 defer bCleanup() 456 457 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 458 defer configCleanup() 459 460 // Set the tfe client's RemoteAPIVersion to an empty string, to mimic 461 // API versions prior to 2.3. 462 b.client.SetFakeRemoteAPIVersion("") 463 464 addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") 465 466 op.Targets = []addrs.Targetable{addr} 467 op.Workspace = backend.DefaultStateName 468 469 run, err := b.Operation(context.Background(), op) 470 if err != nil { 471 t.Fatalf("error starting operation: %v", err) 472 } 473 474 <-run.Done() 475 output := done(t) 476 if run.Result == backend.OperationSuccess { 477 t.Fatal("expected apply operation to fail") 478 } 479 if !run.PlanEmpty { 480 t.Fatalf("expected plan to be empty") 481 } 482 483 errOutput := output.Stderr() 484 if !strings.Contains(errOutput, "Resource targeting is not supported") { 485 t.Fatalf("expected a targeting error, got: %v", errOutput) 486 } 487 } 488 489 func TestRemote_applyWithReplace(t *testing.T) { 490 b, bCleanup := testBackendDefault(t) 491 defer bCleanup() 492 493 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 494 defer configCleanup() 495 defer done(t) 496 497 addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") 498 499 op.ForceReplace = []addrs.AbsResourceInstance{addr} 500 op.Workspace = backend.DefaultStateName 501 502 run, err := b.Operation(context.Background(), op) 503 if err != nil { 504 t.Fatalf("error starting operation: %v", err) 505 } 506 507 <-run.Done() 508 if run.Result != backend.OperationSuccess { 509 t.Fatal("expected plan operation to succeed") 510 } 511 if run.PlanEmpty { 512 t.Fatalf("expected plan to be non-empty") 513 } 514 515 // We should find a run inside the mock client that has the same 516 // refresh address we requested above. 517 runsAPI := b.client.Runs.(*mockRuns) 518 if got, want := len(runsAPI.runs), 1; got != want { 519 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 520 } 521 for _, run := range runsAPI.runs { 522 if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" { 523 t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff) 524 } 525 } 526 } 527 528 func TestRemote_applyWithReplaceIncompatibleAPIVersion(t *testing.T) { 529 b, bCleanup := testBackendDefault(t) 530 defer bCleanup() 531 532 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 533 defer configCleanup() 534 535 b.client.SetFakeRemoteAPIVersion("2.3") 536 537 addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") 538 539 op.ForceReplace = []addrs.AbsResourceInstance{addr} 540 op.Workspace = backend.DefaultStateName 541 542 run, err := b.Operation(context.Background(), op) 543 if err != nil { 544 t.Fatalf("error starting operation: %v", err) 545 } 546 547 <-run.Done() 548 output := done(t) 549 if run.Result == backend.OperationSuccess { 550 t.Fatal("expected apply operation to fail") 551 } 552 if !run.PlanEmpty { 553 t.Fatalf("expected plan to be empty") 554 } 555 556 errOutput := output.Stderr() 557 if !strings.Contains(errOutput, "Planning resource replacements is not supported") { 558 t.Fatalf("expected a not supported error, got: %v", errOutput) 559 } 560 } 561 562 func TestRemote_applyWithVariables(t *testing.T) { 563 b, bCleanup := testBackendDefault(t) 564 defer bCleanup() 565 566 op, configCleanup, done := testOperationApply(t, "./testdata/apply-variables") 567 defer configCleanup() 568 569 op.Variables = testVariables(terraform.ValueFromNamedFile, "foo", "bar") 570 op.Workspace = backend.DefaultStateName 571 572 run, err := b.Operation(context.Background(), op) 573 if err != nil { 574 t.Fatalf("error starting operation: %v", err) 575 } 576 577 <-run.Done() 578 output := done(t) 579 if run.Result == backend.OperationSuccess { 580 t.Fatal("expected apply operation to fail") 581 } 582 583 errOutput := output.Stderr() 584 if !strings.Contains(errOutput, "variables are currently not supported") { 585 t.Fatalf("expected a variables error, got: %v", errOutput) 586 } 587 } 588 589 func TestRemote_applyNoConfig(t *testing.T) { 590 b, bCleanup := testBackendDefault(t) 591 defer bCleanup() 592 593 op, configCleanup, done := testOperationApply(t, "./testdata/empty") 594 defer configCleanup() 595 596 op.Workspace = backend.DefaultStateName 597 598 run, err := b.Operation(context.Background(), op) 599 if err != nil { 600 t.Fatalf("error starting operation: %v", err) 601 } 602 603 <-run.Done() 604 output := done(t) 605 if run.Result == backend.OperationSuccess { 606 t.Fatal("expected apply operation to fail") 607 } 608 if !run.PlanEmpty { 609 t.Fatalf("expected plan to be empty") 610 } 611 612 errOutput := output.Stderr() 613 if !strings.Contains(errOutput, "configuration files found") { 614 t.Fatalf("expected configuration files error, got: %v", errOutput) 615 } 616 617 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 618 // An error suggests that the state was not unlocked after apply 619 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 620 t.Fatalf("unexpected error locking state after failed apply: %s", err.Error()) 621 } 622 } 623 624 func TestRemote_applyNoChanges(t *testing.T) { 625 b, bCleanup := testBackendDefault(t) 626 defer bCleanup() 627 628 op, configCleanup, done := testOperationApply(t, "./testdata/apply-no-changes") 629 defer configCleanup() 630 defer done(t) 631 632 op.Workspace = backend.DefaultStateName 633 634 run, err := b.Operation(context.Background(), op) 635 if err != nil { 636 t.Fatalf("error starting operation: %v", err) 637 } 638 639 <-run.Done() 640 if run.Result != backend.OperationSuccess { 641 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 642 } 643 if !run.PlanEmpty { 644 t.Fatalf("expected plan to be empty") 645 } 646 647 output := b.CLI.(*cli.MockUi).OutputWriter.String() 648 if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") { 649 t.Fatalf("expected no changes in plan summery: %s", output) 650 } 651 if !strings.Contains(output, "Sentinel Result: true") { 652 t.Fatalf("expected policy check result in output: %s", output) 653 } 654 } 655 656 func TestRemote_applyNoApprove(t *testing.T) { 657 b, bCleanup := testBackendDefault(t) 658 defer bCleanup() 659 660 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 661 defer configCleanup() 662 663 input := testInput(t, map[string]string{ 664 "approve": "no", 665 }) 666 667 op.UIIn = input 668 op.UIOut = b.CLI 669 op.Workspace = backend.DefaultStateName 670 671 run, err := b.Operation(context.Background(), op) 672 if err != nil { 673 t.Fatalf("error starting operation: %v", err) 674 } 675 676 <-run.Done() 677 output := done(t) 678 if run.Result == backend.OperationSuccess { 679 t.Fatal("expected apply operation to fail") 680 } 681 if !run.PlanEmpty { 682 t.Fatalf("expected plan to be empty") 683 } 684 685 if len(input.answers) > 0 { 686 t.Fatalf("expected no unused answers, got: %v", input.answers) 687 } 688 689 errOutput := output.Stderr() 690 if !strings.Contains(errOutput, "Apply discarded") { 691 t.Fatalf("expected an apply discarded error, got: %v", errOutput) 692 } 693 } 694 695 func TestRemote_applyAutoApprove(t *testing.T) { 696 b, bCleanup := testBackendDefault(t) 697 defer bCleanup() 698 699 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 700 defer configCleanup() 701 defer done(t) 702 703 input := testInput(t, map[string]string{ 704 "approve": "no", 705 }) 706 707 op.AutoApprove = true 708 op.UIIn = input 709 op.UIOut = b.CLI 710 op.Workspace = backend.DefaultStateName 711 712 run, err := b.Operation(context.Background(), op) 713 if err != nil { 714 t.Fatalf("error starting operation: %v", err) 715 } 716 717 <-run.Done() 718 if run.Result != backend.OperationSuccess { 719 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 720 } 721 if run.PlanEmpty { 722 t.Fatalf("expected a non-empty plan") 723 } 724 725 if len(input.answers) != 1 { 726 t.Fatalf("expected an unused answer, got: %v", input.answers) 727 } 728 729 output := b.CLI.(*cli.MockUi).OutputWriter.String() 730 if !strings.Contains(output, "Running apply in the remote backend") { 731 t.Fatalf("expected remote backend header in output: %s", output) 732 } 733 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 734 t.Fatalf("expected plan summery in output: %s", output) 735 } 736 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 737 t.Fatalf("expected apply summery in output: %s", output) 738 } 739 } 740 741 func TestRemote_applyApprovedExternally(t *testing.T) { 742 b, bCleanup := testBackendDefault(t) 743 defer bCleanup() 744 745 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 746 defer configCleanup() 747 defer done(t) 748 749 input := testInput(t, map[string]string{ 750 "approve": "wait-for-external-update", 751 }) 752 753 op.UIIn = input 754 op.UIOut = b.CLI 755 op.Workspace = backend.DefaultStateName 756 757 ctx := context.Background() 758 759 run, err := b.Operation(ctx, op) 760 if err != nil { 761 t.Fatalf("error starting operation: %v", err) 762 } 763 764 // Wait 50 milliseconds to make sure the run started. 765 time.Sleep(50 * time.Millisecond) 766 767 wl, err := b.client.Workspaces.List( 768 ctx, 769 b.organization, 770 tfe.WorkspaceListOptions{ 771 ListOptions: tfe.ListOptions{PageNumber: 2, PageSize: 10}, 772 }, 773 ) 774 if err != nil { 775 t.Fatalf("unexpected error listing workspaces: %v", err) 776 } 777 if len(wl.Items) != 1 { 778 t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items)) 779 } 780 781 rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, tfe.RunListOptions{}) 782 if err != nil { 783 t.Fatalf("unexpected error listing runs: %v", err) 784 } 785 if len(rl.Items) != 1 { 786 t.Fatalf("expected 1 run, got %d runs", len(rl.Items)) 787 } 788 789 err = b.client.Runs.Apply(context.Background(), rl.Items[0].ID, tfe.RunApplyOptions{}) 790 if err != nil { 791 t.Fatalf("unexpected error approving run: %v", err) 792 } 793 794 <-run.Done() 795 if run.Result != backend.OperationSuccess { 796 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 797 } 798 if run.PlanEmpty { 799 t.Fatalf("expected a non-empty plan") 800 } 801 802 output := b.CLI.(*cli.MockUi).OutputWriter.String() 803 if !strings.Contains(output, "Running apply in the remote backend") { 804 t.Fatalf("expected remote backend header in output: %s", output) 805 } 806 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 807 t.Fatalf("expected plan summery in output: %s", output) 808 } 809 if !strings.Contains(output, "approved using the UI or API") { 810 t.Fatalf("expected external approval in output: %s", output) 811 } 812 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 813 t.Fatalf("expected apply summery in output: %s", output) 814 } 815 } 816 817 func TestRemote_applyDiscardedExternally(t *testing.T) { 818 b, bCleanup := testBackendDefault(t) 819 defer bCleanup() 820 821 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 822 defer configCleanup() 823 defer done(t) 824 825 input := testInput(t, map[string]string{ 826 "approve": "wait-for-external-update", 827 }) 828 829 op.UIIn = input 830 op.UIOut = b.CLI 831 op.Workspace = backend.DefaultStateName 832 833 ctx := context.Background() 834 835 run, err := b.Operation(ctx, op) 836 if err != nil { 837 t.Fatalf("error starting operation: %v", err) 838 } 839 840 // Wait 50 milliseconds to make sure the run started. 841 time.Sleep(50 * time.Millisecond) 842 843 wl, err := b.client.Workspaces.List( 844 ctx, 845 b.organization, 846 tfe.WorkspaceListOptions{ 847 ListOptions: tfe.ListOptions{PageNumber: 2, PageSize: 10}, 848 }, 849 ) 850 if err != nil { 851 t.Fatalf("unexpected error listing workspaces: %v", err) 852 } 853 if len(wl.Items) != 1 { 854 t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items)) 855 } 856 857 rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, tfe.RunListOptions{}) 858 if err != nil { 859 t.Fatalf("unexpected error listing runs: %v", err) 860 } 861 if len(rl.Items) != 1 { 862 t.Fatalf("expected 1 run, got %d runs", len(rl.Items)) 863 } 864 865 err = b.client.Runs.Discard(context.Background(), rl.Items[0].ID, tfe.RunDiscardOptions{}) 866 if err != nil { 867 t.Fatalf("unexpected error discarding run: %v", err) 868 } 869 870 <-run.Done() 871 if run.Result == backend.OperationSuccess { 872 t.Fatal("expected apply operation to fail") 873 } 874 if !run.PlanEmpty { 875 t.Fatalf("expected plan to be empty") 876 } 877 878 output := b.CLI.(*cli.MockUi).OutputWriter.String() 879 if !strings.Contains(output, "Running apply in the remote backend") { 880 t.Fatalf("expected remote backend header in output: %s", output) 881 } 882 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 883 t.Fatalf("expected plan summery in output: %s", output) 884 } 885 if !strings.Contains(output, "discarded using the UI or API") { 886 t.Fatalf("expected external discard output: %s", output) 887 } 888 if strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 889 t.Fatalf("unexpected apply summery in output: %s", output) 890 } 891 } 892 893 func TestRemote_applyWithAutoApply(t *testing.T) { 894 b, bCleanup := testBackendNoDefault(t) 895 defer bCleanup() 896 897 // Create a named workspace that auto applies. 898 _, err := b.client.Workspaces.Create( 899 context.Background(), 900 b.organization, 901 tfe.WorkspaceCreateOptions{ 902 AutoApply: tfe.Bool(true), 903 Name: tfe.String(b.prefix + "prod"), 904 }, 905 ) 906 if err != nil { 907 t.Fatalf("error creating named workspace: %v", err) 908 } 909 910 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 911 defer configCleanup() 912 defer done(t) 913 914 input := testInput(t, map[string]string{ 915 "approve": "yes", 916 }) 917 918 op.UIIn = input 919 op.UIOut = b.CLI 920 op.Workspace = "prod" 921 922 run, err := b.Operation(context.Background(), op) 923 if err != nil { 924 t.Fatalf("error starting operation: %v", err) 925 } 926 927 <-run.Done() 928 if run.Result != backend.OperationSuccess { 929 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 930 } 931 if run.PlanEmpty { 932 t.Fatalf("expected a non-empty plan") 933 } 934 935 if len(input.answers) != 1 { 936 t.Fatalf("expected an unused answer, got: %v", input.answers) 937 } 938 939 output := b.CLI.(*cli.MockUi).OutputWriter.String() 940 if !strings.Contains(output, "Running apply in the remote backend") { 941 t.Fatalf("expected remote backend header in output: %s", output) 942 } 943 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 944 t.Fatalf("expected plan summery in output: %s", output) 945 } 946 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 947 t.Fatalf("expected apply summery in output: %s", output) 948 } 949 } 950 951 func TestRemote_applyForceLocal(t *testing.T) { 952 // Set TF_FORCE_LOCAL_BACKEND so the remote backend will use 953 // the local backend with itself as embedded backend. 954 if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil { 955 t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err) 956 } 957 defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND") 958 959 b, bCleanup := testBackendDefault(t) 960 defer bCleanup() 961 962 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 963 defer configCleanup() 964 defer done(t) 965 966 input := testInput(t, map[string]string{ 967 "approve": "yes", 968 }) 969 970 op.UIIn = input 971 op.UIOut = b.CLI 972 op.Workspace = backend.DefaultStateName 973 974 streams, done := terminal.StreamsForTesting(t) 975 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 976 op.View = view 977 978 run, err := b.Operation(context.Background(), op) 979 if err != nil { 980 t.Fatalf("error starting operation: %v", err) 981 } 982 983 <-run.Done() 984 if run.Result != backend.OperationSuccess { 985 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 986 } 987 if run.PlanEmpty { 988 t.Fatalf("expected a non-empty plan") 989 } 990 991 if len(input.answers) > 0 { 992 t.Fatalf("expected no unused answers, got: %v", input.answers) 993 } 994 995 output := b.CLI.(*cli.MockUi).OutputWriter.String() 996 if strings.Contains(output, "Running apply in the remote backend") { 997 t.Fatalf("unexpected remote backend header in output: %s", output) 998 } 999 if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1000 t.Fatalf("expected plan summary in output: %s", output) 1001 } 1002 if !run.State.HasResources() { 1003 t.Fatalf("expected resources in state") 1004 } 1005 } 1006 1007 func TestRemote_applyWorkspaceWithoutOperations(t *testing.T) { 1008 b, bCleanup := testBackendNoDefault(t) 1009 defer bCleanup() 1010 1011 ctx := context.Background() 1012 1013 // Create a named workspace that doesn't allow operations. 1014 _, err := b.client.Workspaces.Create( 1015 ctx, 1016 b.organization, 1017 tfe.WorkspaceCreateOptions{ 1018 Name: tfe.String(b.prefix + "no-operations"), 1019 }, 1020 ) 1021 if err != nil { 1022 t.Fatalf("error creating named workspace: %v", err) 1023 } 1024 1025 op, configCleanup, done := testOperationApply(t, "./testdata/apply") 1026 defer configCleanup() 1027 defer done(t) 1028 1029 input := testInput(t, map[string]string{ 1030 "approve": "yes", 1031 }) 1032 1033 op.UIIn = input 1034 op.UIOut = b.CLI 1035 op.Workspace = "no-operations" 1036 1037 streams, done := terminal.StreamsForTesting(t) 1038 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 1039 op.View = view 1040 1041 run, err := b.Operation(ctx, op) 1042 if err != nil { 1043 t.Fatalf("error starting operation: %v", err) 1044 } 1045 1046 <-run.Done() 1047 if run.Result != backend.OperationSuccess { 1048 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1049 } 1050 if run.PlanEmpty { 1051 t.Fatalf("expected a non-empty plan") 1052 } 1053 1054 if len(input.answers) > 0 { 1055 t.Fatalf("expected no unused answers, got: %v", input.answers) 1056 } 1057 1058 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1059 if strings.Contains(output, "Running apply in the remote backend") { 1060 t.Fatalf("unexpected remote backend header in output: %s", output) 1061 } 1062 if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1063 t.Fatalf("expected plan summary in output: %s", output) 1064 } 1065 if !run.State.HasResources() { 1066 t.Fatalf("expected resources in state") 1067 } 1068 } 1069 1070 func TestRemote_applyLockTimeout(t *testing.T) { 1071 b, bCleanup := testBackendDefault(t) 1072 defer bCleanup() 1073 1074 ctx := context.Background() 1075 1076 // Retrieve the workspace used to run this operation in. 1077 w, err := b.client.Workspaces.Read(ctx, b.organization, b.workspace) 1078 if err != nil { 1079 t.Fatalf("error retrieving workspace: %v", err) 1080 } 1081 1082 // Create a new configuration version. 1083 c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{}) 1084 if err != nil { 1085 t.Fatalf("error creating configuration version: %v", err) 1086 } 1087 1088 // Create a pending run to block this run. 1089 _, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{ 1090 ConfigurationVersion: c, 1091 Workspace: w, 1092 }) 1093 if err != nil { 1094 t.Fatalf("error creating pending run: %v", err) 1095 } 1096 1097 op, configCleanup, done := testOperationApplyWithTimeout(t, "./testdata/apply", 50*time.Millisecond) 1098 defer configCleanup() 1099 defer done(t) 1100 1101 input := testInput(t, map[string]string{ 1102 "cancel": "yes", 1103 "approve": "yes", 1104 }) 1105 1106 op.UIIn = input 1107 op.UIOut = b.CLI 1108 op.Workspace = backend.DefaultStateName 1109 1110 _, err = b.Operation(context.Background(), op) 1111 if err != nil { 1112 t.Fatalf("error starting operation: %v", err) 1113 } 1114 1115 sigint := make(chan os.Signal, 1) 1116 signal.Notify(sigint, syscall.SIGINT) 1117 select { 1118 case <-sigint: 1119 // Stop redirecting SIGINT signals. 1120 signal.Stop(sigint) 1121 case <-time.After(200 * time.Millisecond): 1122 t.Fatalf("expected lock timeout after 50 milliseconds, waited 200 milliseconds") 1123 } 1124 1125 if len(input.answers) != 2 { 1126 t.Fatalf("expected unused answers, got: %v", input.answers) 1127 } 1128 1129 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1130 if !strings.Contains(output, "Running apply in the remote backend") { 1131 t.Fatalf("expected remote backend header in output: %s", output) 1132 } 1133 if !strings.Contains(output, "Lock timeout exceeded") { 1134 t.Fatalf("expected lock timout error in output: %s", output) 1135 } 1136 if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1137 t.Fatalf("unexpected plan summery in output: %s", output) 1138 } 1139 if strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1140 t.Fatalf("unexpected apply summery in output: %s", output) 1141 } 1142 } 1143 1144 func TestRemote_applyDestroy(t *testing.T) { 1145 b, bCleanup := testBackendDefault(t) 1146 defer bCleanup() 1147 1148 op, configCleanup, done := testOperationApply(t, "./testdata/apply-destroy") 1149 defer configCleanup() 1150 defer done(t) 1151 1152 input := testInput(t, map[string]string{ 1153 "approve": "yes", 1154 }) 1155 1156 op.PlanMode = plans.DestroyMode 1157 op.UIIn = input 1158 op.UIOut = b.CLI 1159 op.Workspace = backend.DefaultStateName 1160 1161 run, err := b.Operation(context.Background(), op) 1162 if err != nil { 1163 t.Fatalf("error starting operation: %v", err) 1164 } 1165 1166 <-run.Done() 1167 if run.Result != backend.OperationSuccess { 1168 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1169 } 1170 if run.PlanEmpty { 1171 t.Fatalf("expected a non-empty plan") 1172 } 1173 1174 if len(input.answers) > 0 { 1175 t.Fatalf("expected no unused answers, got: %v", input.answers) 1176 } 1177 1178 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1179 if !strings.Contains(output, "Running apply in the remote backend") { 1180 t.Fatalf("expected remote backend header in output: %s", output) 1181 } 1182 if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") { 1183 t.Fatalf("expected plan summery in output: %s", output) 1184 } 1185 if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") { 1186 t.Fatalf("expected apply summery in output: %s", output) 1187 } 1188 } 1189 1190 func TestRemote_applyDestroyNoConfig(t *testing.T) { 1191 b, bCleanup := testBackendDefault(t) 1192 defer bCleanup() 1193 1194 input := testInput(t, map[string]string{ 1195 "approve": "yes", 1196 }) 1197 1198 op, configCleanup, done := testOperationApply(t, "./testdata/empty") 1199 defer configCleanup() 1200 defer done(t) 1201 1202 op.PlanMode = plans.DestroyMode 1203 op.UIIn = input 1204 op.UIOut = b.CLI 1205 op.Workspace = backend.DefaultStateName 1206 1207 run, err := b.Operation(context.Background(), op) 1208 if err != nil { 1209 t.Fatalf("error starting operation: %v", err) 1210 } 1211 1212 <-run.Done() 1213 if run.Result != backend.OperationSuccess { 1214 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1215 } 1216 if run.PlanEmpty { 1217 t.Fatalf("expected a non-empty plan") 1218 } 1219 1220 if len(input.answers) > 0 { 1221 t.Fatalf("expected no unused answers, got: %v", input.answers) 1222 } 1223 } 1224 1225 func TestRemote_applyPolicyPass(t *testing.T) { 1226 b, bCleanup := testBackendDefault(t) 1227 defer bCleanup() 1228 1229 op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-passed") 1230 defer configCleanup() 1231 defer done(t) 1232 1233 input := testInput(t, map[string]string{ 1234 "approve": "yes", 1235 }) 1236 1237 op.UIIn = input 1238 op.UIOut = b.CLI 1239 op.Workspace = backend.DefaultStateName 1240 1241 run, err := b.Operation(context.Background(), op) 1242 if err != nil { 1243 t.Fatalf("error starting operation: %v", err) 1244 } 1245 1246 <-run.Done() 1247 if run.Result != backend.OperationSuccess { 1248 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1249 } 1250 if run.PlanEmpty { 1251 t.Fatalf("expected a non-empty plan") 1252 } 1253 1254 if len(input.answers) > 0 { 1255 t.Fatalf("expected no unused answers, got: %v", input.answers) 1256 } 1257 1258 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1259 if !strings.Contains(output, "Running apply in the remote backend") { 1260 t.Fatalf("expected remote backend header in output: %s", output) 1261 } 1262 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1263 t.Fatalf("expected plan summery in output: %s", output) 1264 } 1265 if !strings.Contains(output, "Sentinel Result: true") { 1266 t.Fatalf("expected policy check result in output: %s", output) 1267 } 1268 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1269 t.Fatalf("expected apply summery in output: %s", output) 1270 } 1271 } 1272 1273 func TestRemote_applyPolicyHardFail(t *testing.T) { 1274 b, bCleanup := testBackendDefault(t) 1275 defer bCleanup() 1276 1277 op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-hard-failed") 1278 defer configCleanup() 1279 1280 input := testInput(t, map[string]string{ 1281 "approve": "yes", 1282 }) 1283 1284 op.UIIn = input 1285 op.UIOut = b.CLI 1286 op.Workspace = backend.DefaultStateName 1287 1288 run, err := b.Operation(context.Background(), op) 1289 if err != nil { 1290 t.Fatalf("error starting operation: %v", err) 1291 } 1292 1293 <-run.Done() 1294 viewOutput := done(t) 1295 if run.Result == backend.OperationSuccess { 1296 t.Fatal("expected apply operation to fail") 1297 } 1298 if !run.PlanEmpty { 1299 t.Fatalf("expected plan to be empty") 1300 } 1301 1302 if len(input.answers) != 1 { 1303 t.Fatalf("expected an unused answers, got: %v", input.answers) 1304 } 1305 1306 errOutput := viewOutput.Stderr() 1307 if !strings.Contains(errOutput, "hard failed") { 1308 t.Fatalf("expected a policy check error, got: %v", errOutput) 1309 } 1310 1311 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1312 if !strings.Contains(output, "Running apply in the remote backend") { 1313 t.Fatalf("expected remote backend header in output: %s", output) 1314 } 1315 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1316 t.Fatalf("expected plan summery in output: %s", output) 1317 } 1318 if !strings.Contains(output, "Sentinel Result: false") { 1319 t.Fatalf("expected policy check result in output: %s", output) 1320 } 1321 if strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1322 t.Fatalf("unexpected apply summery in output: %s", output) 1323 } 1324 } 1325 1326 func TestRemote_applyPolicySoftFail(t *testing.T) { 1327 b, bCleanup := testBackendDefault(t) 1328 defer bCleanup() 1329 1330 op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed") 1331 defer configCleanup() 1332 defer done(t) 1333 1334 input := testInput(t, map[string]string{ 1335 "override": "override", 1336 "approve": "yes", 1337 }) 1338 1339 op.AutoApprove = false 1340 op.UIIn = input 1341 op.UIOut = b.CLI 1342 op.Workspace = backend.DefaultStateName 1343 1344 run, err := b.Operation(context.Background(), op) 1345 if err != nil { 1346 t.Fatalf("error starting operation: %v", err) 1347 } 1348 1349 <-run.Done() 1350 if run.Result != backend.OperationSuccess { 1351 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1352 } 1353 if run.PlanEmpty { 1354 t.Fatalf("expected a non-empty plan") 1355 } 1356 1357 if len(input.answers) > 0 { 1358 t.Fatalf("expected no unused answers, got: %v", input.answers) 1359 } 1360 1361 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1362 if !strings.Contains(output, "Running apply in the remote backend") { 1363 t.Fatalf("expected remote backend header in output: %s", output) 1364 } 1365 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1366 t.Fatalf("expected plan summery in output: %s", output) 1367 } 1368 if !strings.Contains(output, "Sentinel Result: false") { 1369 t.Fatalf("expected policy check result in output: %s", output) 1370 } 1371 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1372 t.Fatalf("expected apply summery in output: %s", output) 1373 } 1374 } 1375 1376 func TestRemote_applyPolicySoftFailAutoApproveSuccess(t *testing.T) { 1377 b, bCleanup := testBackendDefault(t) 1378 defer bCleanup() 1379 1380 op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed") 1381 defer configCleanup() 1382 1383 input := testInput(t, map[string]string{}) 1384 1385 op.AutoApprove = true 1386 op.UIIn = input 1387 op.UIOut = b.CLI 1388 op.Workspace = backend.DefaultStateName 1389 1390 run, err := b.Operation(context.Background(), op) 1391 if err != nil { 1392 t.Fatalf("error starting operation: %v", err) 1393 } 1394 1395 <-run.Done() 1396 viewOutput := done(t) 1397 if run.Result != backend.OperationSuccess { 1398 t.Fatal("expected apply operation to success due to auto-approve") 1399 } 1400 1401 if run.PlanEmpty { 1402 t.Fatalf("expected plan to not be empty, plan opertion completed without error") 1403 } 1404 1405 if len(input.answers) != 0 { 1406 t.Fatalf("expected no answers, got: %v", input.answers) 1407 } 1408 1409 errOutput := viewOutput.Stderr() 1410 if strings.Contains(errOutput, "soft failed") { 1411 t.Fatalf("expected no policy check errors, instead got: %v", errOutput) 1412 } 1413 1414 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1415 if !strings.Contains(output, "Sentinel Result: false") { 1416 t.Fatalf("expected policy check to be false, insead got: %s", output) 1417 } 1418 if !strings.Contains(output, "Apply complete!") { 1419 t.Fatalf("expected apply to be complete, instead got: %s", output) 1420 } 1421 1422 if !strings.Contains(output, "Resources: 1 added, 0 changed, 0 destroyed") { 1423 t.Fatalf("expected resources, instead got: %s", output) 1424 } 1425 } 1426 1427 func TestRemote_applyPolicySoftFailAutoApply(t *testing.T) { 1428 b, bCleanup := testBackendDefault(t) 1429 defer bCleanup() 1430 1431 // Create a named workspace that auto applies. 1432 _, err := b.client.Workspaces.Create( 1433 context.Background(), 1434 b.organization, 1435 tfe.WorkspaceCreateOptions{ 1436 AutoApply: tfe.Bool(true), 1437 Name: tfe.String(b.prefix + "prod"), 1438 }, 1439 ) 1440 if err != nil { 1441 t.Fatalf("error creating named workspace: %v", err) 1442 } 1443 1444 op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed") 1445 defer configCleanup() 1446 defer done(t) 1447 1448 input := testInput(t, map[string]string{ 1449 "override": "override", 1450 "approve": "yes", 1451 }) 1452 1453 op.UIIn = input 1454 op.UIOut = b.CLI 1455 op.Workspace = "prod" 1456 1457 run, err := b.Operation(context.Background(), op) 1458 if err != nil { 1459 t.Fatalf("error starting operation: %v", err) 1460 } 1461 1462 <-run.Done() 1463 if run.Result != backend.OperationSuccess { 1464 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1465 } 1466 if run.PlanEmpty { 1467 t.Fatalf("expected a non-empty plan") 1468 } 1469 1470 if len(input.answers) != 1 { 1471 t.Fatalf("expected an unused answer, got: %v", input.answers) 1472 } 1473 1474 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1475 if !strings.Contains(output, "Running apply in the remote backend") { 1476 t.Fatalf("expected remote backend header in output: %s", output) 1477 } 1478 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1479 t.Fatalf("expected plan summery in output: %s", output) 1480 } 1481 if !strings.Contains(output, "Sentinel Result: false") { 1482 t.Fatalf("expected policy check result in output: %s", output) 1483 } 1484 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1485 t.Fatalf("expected apply summery in output: %s", output) 1486 } 1487 } 1488 1489 func TestRemote_applyWithRemoteError(t *testing.T) { 1490 b, bCleanup := testBackendDefault(t) 1491 defer bCleanup() 1492 1493 op, configCleanup, done := testOperationApply(t, "./testdata/apply-with-error") 1494 defer configCleanup() 1495 defer done(t) 1496 1497 op.Workspace = backend.DefaultStateName 1498 1499 run, err := b.Operation(context.Background(), op) 1500 if err != nil { 1501 t.Fatalf("error starting operation: %v", err) 1502 } 1503 1504 <-run.Done() 1505 if run.Result == backend.OperationSuccess { 1506 t.Fatal("expected apply operation to fail") 1507 } 1508 if run.Result.ExitStatus() != 1 { 1509 t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) 1510 } 1511 1512 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1513 if !strings.Contains(output, "null_resource.foo: 1 error") { 1514 t.Fatalf("expected apply error in output: %s", output) 1515 } 1516 } 1517 1518 func TestRemote_applyVersionCheck(t *testing.T) { 1519 testCases := map[string]struct { 1520 localVersion string 1521 remoteVersion string 1522 forceLocal bool 1523 hasOperations bool 1524 wantErr string 1525 }{ 1526 "versions can be different for remote apply": { 1527 localVersion: "0.14.0", 1528 remoteVersion: "0.13.5", 1529 hasOperations: true, 1530 }, 1531 "versions can be different for local apply": { 1532 localVersion: "0.14.0", 1533 remoteVersion: "0.13.5", 1534 hasOperations: false, 1535 }, 1536 "force local with remote operations and different versions is acceptable": { 1537 localVersion: "0.14.0", 1538 remoteVersion: "0.14.0-acme-provider-bundle", 1539 forceLocal: true, 1540 hasOperations: true, 1541 }, 1542 "no error if versions are identical": { 1543 localVersion: "0.14.0", 1544 remoteVersion: "0.14.0", 1545 forceLocal: true, 1546 hasOperations: true, 1547 }, 1548 "no error if force local but workspace has remote operations disabled": { 1549 localVersion: "0.14.0", 1550 remoteVersion: "0.13.5", 1551 forceLocal: true, 1552 hasOperations: false, 1553 }, 1554 } 1555 1556 for name, tc := range testCases { 1557 t.Run(name, func(t *testing.T) { 1558 b, bCleanup := testBackendDefault(t) 1559 defer bCleanup() 1560 1561 // SETUP: Save original local version state and restore afterwards 1562 p := tfversion.Prerelease 1563 v := tfversion.Version 1564 s := tfversion.SemVer 1565 defer func() { 1566 tfversion.Prerelease = p 1567 tfversion.Version = v 1568 tfversion.SemVer = s 1569 }() 1570 1571 // SETUP: Set local version for the test case 1572 tfversion.Prerelease = "" 1573 tfversion.Version = tc.localVersion 1574 tfversion.SemVer = version.Must(version.NewSemver(tc.localVersion)) 1575 1576 // SETUP: Set force local for the test case 1577 b.forceLocal = tc.forceLocal 1578 1579 ctx := context.Background() 1580 1581 // SETUP: set the operations and Terraform Version fields on the 1582 // remote workspace 1583 _, err := b.client.Workspaces.Update( 1584 ctx, 1585 b.organization, 1586 b.workspace, 1587 tfe.WorkspaceUpdateOptions{ 1588 Operations: tfe.Bool(tc.hasOperations), 1589 TerraformVersion: tfe.String(tc.remoteVersion), 1590 }, 1591 ) 1592 if err != nil { 1593 t.Fatalf("error creating named workspace: %v", err) 1594 } 1595 1596 // RUN: prepare the apply operation and run it 1597 op, configCleanup, _ := testOperationApply(t, "./testdata/apply") 1598 defer configCleanup() 1599 1600 streams, done := terminal.StreamsForTesting(t) 1601 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 1602 op.View = view 1603 1604 input := testInput(t, map[string]string{ 1605 "approve": "yes", 1606 }) 1607 1608 op.UIIn = input 1609 op.UIOut = b.CLI 1610 op.Workspace = backend.DefaultStateName 1611 1612 run, err := b.Operation(ctx, op) 1613 if err != nil { 1614 t.Fatalf("error starting operation: %v", err) 1615 } 1616 1617 // RUN: wait for completion 1618 <-run.Done() 1619 output := done(t) 1620 1621 if tc.wantErr != "" { 1622 // ASSERT: if the test case wants an error, check for failure 1623 // and the error message 1624 if run.Result != backend.OperationFailure { 1625 t.Fatalf("expected run to fail, but result was %#v", run.Result) 1626 } 1627 errOutput := output.Stderr() 1628 if !strings.Contains(errOutput, tc.wantErr) { 1629 t.Fatalf("missing error %q\noutput: %s", tc.wantErr, errOutput) 1630 } 1631 } else { 1632 // ASSERT: otherwise, check for success and appropriate output 1633 // based on whether the run should be local or remote 1634 if run.Result != backend.OperationSuccess { 1635 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1636 } 1637 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1638 hasRemote := strings.Contains(output, "Running apply in the remote backend") 1639 hasSummary := strings.Contains(output, "1 added, 0 changed, 0 destroyed") 1640 hasResources := run.State.HasResources() 1641 if !tc.forceLocal && tc.hasOperations { 1642 if !hasRemote { 1643 t.Errorf("missing remote backend header in output: %s", output) 1644 } 1645 if !hasSummary { 1646 t.Errorf("expected apply summary in output: %s", output) 1647 } 1648 } else { 1649 if hasRemote { 1650 t.Errorf("unexpected remote backend header in output: %s", output) 1651 } 1652 if !hasResources { 1653 t.Errorf("expected resources in state") 1654 } 1655 } 1656 } 1657 }) 1658 } 1659 }