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