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