github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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 "github.com/hashicorp/terraform/addrs" 15 "github.com/hashicorp/terraform/backend" 16 "github.com/hashicorp/terraform/internal/initwd" 17 "github.com/hashicorp/terraform/plans/planfile" 18 "github.com/hashicorp/terraform/terraform" 19 "github.com/mitchellh/cli" 20 ) 21 22 func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func()) { 23 t.Helper() 24 25 _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) 26 27 return &backend.Operation{ 28 ConfigDir: configDir, 29 ConfigLoader: configLoader, 30 Parallelism: defaultParallelism, 31 PlanRefresh: true, 32 Type: backend.OperationTypeApply, 33 }, configCleanup 34 } 35 36 func TestRemote_applyBasic(t *testing.T) { 37 b, bCleanup := testBackendDefault(t) 38 defer bCleanup() 39 40 op, configCleanup := testOperationApply(t, "./testdata/apply") 41 defer configCleanup() 42 43 input := testInput(t, map[string]string{ 44 "approve": "yes", 45 }) 46 47 op.UIIn = input 48 op.UIOut = b.CLI 49 op.Workspace = backend.DefaultStateName 50 51 run, err := b.Operation(context.Background(), op) 52 if err != nil { 53 t.Fatalf("error starting operation: %v", err) 54 } 55 56 <-run.Done() 57 if run.Result != backend.OperationSuccess { 58 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 59 } 60 if run.PlanEmpty { 61 t.Fatalf("expected a non-empty plan") 62 } 63 64 if len(input.answers) > 0 { 65 t.Fatalf("expected no unused answers, got: %v", input.answers) 66 } 67 68 output := b.CLI.(*cli.MockUi).OutputWriter.String() 69 if !strings.Contains(output, "Running apply in the remote backend") { 70 t.Fatalf("expected remote backend header in output: %s", output) 71 } 72 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 73 t.Fatalf("expected plan summery in output: %s", output) 74 } 75 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 76 t.Fatalf("expected apply summery in output: %s", output) 77 } 78 } 79 80 func TestRemote_applyCanceled(t *testing.T) { 81 b, bCleanup := testBackendDefault(t) 82 defer bCleanup() 83 84 op, configCleanup := testOperationApply(t, "./testdata/apply") 85 defer configCleanup() 86 87 op.Workspace = backend.DefaultStateName 88 89 run, err := b.Operation(context.Background(), op) 90 if err != nil { 91 t.Fatalf("error starting operation: %v", err) 92 } 93 94 // Stop the run to simulate a Ctrl-C. 95 run.Stop() 96 97 <-run.Done() 98 if run.Result == backend.OperationSuccess { 99 t.Fatal("expected apply operation to fail") 100 } 101 } 102 103 func TestRemote_applyWithoutPermissions(t *testing.T) { 104 b, bCleanup := testBackendNoDefault(t) 105 defer bCleanup() 106 107 // Create a named workspace without permissions. 108 w, err := b.client.Workspaces.Create( 109 context.Background(), 110 b.organization, 111 tfe.WorkspaceCreateOptions{ 112 Name: tfe.String(b.prefix + "prod"), 113 }, 114 ) 115 if err != nil { 116 t.Fatalf("error creating named workspace: %v", err) 117 } 118 w.Permissions.CanQueueApply = false 119 120 op, configCleanup := testOperationApply(t, "./testdata/apply") 121 defer configCleanup() 122 123 op.UIOut = b.CLI 124 op.Workspace = "prod" 125 126 run, err := b.Operation(context.Background(), op) 127 if err != nil { 128 t.Fatalf("error starting operation: %v", err) 129 } 130 131 <-run.Done() 132 if run.Result == backend.OperationSuccess { 133 t.Fatal("expected apply operation to fail") 134 } 135 136 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 137 if !strings.Contains(errOutput, "Insufficient rights to apply changes") { 138 t.Fatalf("expected a permissions error, got: %v", errOutput) 139 } 140 } 141 142 func TestRemote_applyWithVCS(t *testing.T) { 143 b, bCleanup := testBackendNoDefault(t) 144 defer bCleanup() 145 146 // Create a named workspace with a VCS. 147 _, err := b.client.Workspaces.Create( 148 context.Background(), 149 b.organization, 150 tfe.WorkspaceCreateOptions{ 151 Name: tfe.String(b.prefix + "prod"), 152 VCSRepo: &tfe.VCSRepoOptions{}, 153 }, 154 ) 155 if err != nil { 156 t.Fatalf("error creating named workspace: %v", err) 157 } 158 159 op, configCleanup := testOperationApply(t, "./testdata/apply") 160 defer configCleanup() 161 162 op.Workspace = "prod" 163 164 run, err := b.Operation(context.Background(), op) 165 if err != nil { 166 t.Fatalf("error starting operation: %v", err) 167 } 168 169 <-run.Done() 170 if run.Result == backend.OperationSuccess { 171 t.Fatal("expected apply operation to fail") 172 } 173 if !run.PlanEmpty { 174 t.Fatalf("expected plan to be empty") 175 } 176 177 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 178 if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") { 179 t.Fatalf("expected a VCS error, got: %v", errOutput) 180 } 181 } 182 183 func TestRemote_applyWithParallelism(t *testing.T) { 184 b, bCleanup := testBackendDefault(t) 185 defer bCleanup() 186 187 op, configCleanup := testOperationApply(t, "./testdata/apply") 188 defer configCleanup() 189 190 op.Parallelism = 3 191 op.Workspace = backend.DefaultStateName 192 193 run, err := b.Operation(context.Background(), op) 194 if err != nil { 195 t.Fatalf("error starting operation: %v", err) 196 } 197 198 <-run.Done() 199 if run.Result == backend.OperationSuccess { 200 t.Fatal("expected apply operation to fail") 201 } 202 203 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 204 if !strings.Contains(errOutput, "parallelism values are currently not supported") { 205 t.Fatalf("expected a parallelism error, got: %v", errOutput) 206 } 207 } 208 209 func TestRemote_applyWithPlan(t *testing.T) { 210 b, bCleanup := testBackendDefault(t) 211 defer bCleanup() 212 213 op, configCleanup := testOperationApply(t, "./testdata/apply") 214 defer configCleanup() 215 216 op.PlanFile = &planfile.Reader{} 217 op.Workspace = backend.DefaultStateName 218 219 run, err := b.Operation(context.Background(), op) 220 if err != nil { 221 t.Fatalf("error starting operation: %v", err) 222 } 223 224 <-run.Done() 225 if run.Result == backend.OperationSuccess { 226 t.Fatal("expected apply operation to fail") 227 } 228 if !run.PlanEmpty { 229 t.Fatalf("expected plan to be empty") 230 } 231 232 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 233 if !strings.Contains(errOutput, "saved plan is currently not supported") { 234 t.Fatalf("expected a saved plan error, got: %v", errOutput) 235 } 236 } 237 238 func TestRemote_applyWithoutRefresh(t *testing.T) { 239 b, bCleanup := testBackendDefault(t) 240 defer bCleanup() 241 242 op, configCleanup := testOperationApply(t, "./testdata/apply") 243 defer configCleanup() 244 245 op.PlanRefresh = false 246 op.Workspace = backend.DefaultStateName 247 248 run, err := b.Operation(context.Background(), op) 249 if err != nil { 250 t.Fatalf("error starting operation: %v", err) 251 } 252 253 <-run.Done() 254 if run.Result == backend.OperationSuccess { 255 t.Fatal("expected apply operation to fail") 256 } 257 258 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 259 if !strings.Contains(errOutput, "refresh is currently not supported") { 260 t.Fatalf("expected a refresh error, got: %v", errOutput) 261 } 262 } 263 264 func TestRemote_applyWithTarget(t *testing.T) { 265 b, bCleanup := testBackendDefault(t) 266 defer bCleanup() 267 268 op, configCleanup := testOperationApply(t, "./testdata/apply") 269 defer configCleanup() 270 271 addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") 272 273 op.Targets = []addrs.Targetable{addr} 274 op.Workspace = backend.DefaultStateName 275 276 run, err := b.Operation(context.Background(), op) 277 if err != nil { 278 t.Fatalf("error starting operation: %v", err) 279 } 280 281 <-run.Done() 282 if run.Result != backend.OperationSuccess { 283 t.Fatal("expected apply operation to succeed") 284 } 285 if run.PlanEmpty { 286 t.Fatalf("expected plan to be non-empty") 287 } 288 289 // We should find a run inside the mock client that has the same 290 // target address we requested above. 291 runsAPI := b.client.Runs.(*mockRuns) 292 if got, want := len(runsAPI.runs), 1; got != want { 293 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 294 } 295 for _, run := range runsAPI.runs { 296 if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { 297 t.Errorf("wrong TargetAddrs in the created run\n%s", diff) 298 } 299 } 300 } 301 302 func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) { 303 b, bCleanup := testBackendDefault(t) 304 defer bCleanup() 305 306 op, configCleanup := testOperationPlan(t, "./testdata/plan") 307 defer configCleanup() 308 309 // Set the tfe client's RemoteAPIVersion to an empty string, to mimic 310 // API versions prior to 2.3. 311 b.client.SetFakeRemoteAPIVersion("") 312 313 addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") 314 315 op.Targets = []addrs.Targetable{addr} 316 op.Workspace = backend.DefaultStateName 317 318 run, err := b.Operation(context.Background(), op) 319 if err != nil { 320 t.Fatalf("error starting operation: %v", err) 321 } 322 323 <-run.Done() 324 if run.Result == backend.OperationSuccess { 325 t.Fatal("expected apply operation to fail") 326 } 327 if !run.PlanEmpty { 328 t.Fatalf("expected plan to be empty") 329 } 330 331 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 332 if !strings.Contains(errOutput, "Resource targeting is not supported") { 333 t.Fatalf("expected a targeting error, got: %v", errOutput) 334 } 335 } 336 337 func TestRemote_applyWithVariables(t *testing.T) { 338 b, bCleanup := testBackendDefault(t) 339 defer bCleanup() 340 341 op, configCleanup := testOperationApply(t, "./testdata/apply-variables") 342 defer configCleanup() 343 344 op.Variables = testVariables(terraform.ValueFromNamedFile, "foo", "bar") 345 op.Workspace = backend.DefaultStateName 346 347 run, err := b.Operation(context.Background(), op) 348 if err != nil { 349 t.Fatalf("error starting operation: %v", err) 350 } 351 352 <-run.Done() 353 if run.Result == backend.OperationSuccess { 354 t.Fatal("expected apply operation to fail") 355 } 356 357 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 358 if !strings.Contains(errOutput, "variables are currently not supported") { 359 t.Fatalf("expected a variables error, got: %v", errOutput) 360 } 361 } 362 363 func TestRemote_applyNoConfig(t *testing.T) { 364 b, bCleanup := testBackendDefault(t) 365 defer bCleanup() 366 367 op, configCleanup := testOperationApply(t, "./testdata/empty") 368 defer configCleanup() 369 370 op.Workspace = backend.DefaultStateName 371 372 run, err := b.Operation(context.Background(), op) 373 if err != nil { 374 t.Fatalf("error starting operation: %v", err) 375 } 376 377 <-run.Done() 378 if run.Result == backend.OperationSuccess { 379 t.Fatal("expected apply operation to fail") 380 } 381 if !run.PlanEmpty { 382 t.Fatalf("expected plan to be empty") 383 } 384 385 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 386 if !strings.Contains(errOutput, "configuration files found") { 387 t.Fatalf("expected configuration files error, got: %v", errOutput) 388 } 389 } 390 391 func TestRemote_applyNoChanges(t *testing.T) { 392 b, bCleanup := testBackendDefault(t) 393 defer bCleanup() 394 395 op, configCleanup := testOperationApply(t, "./testdata/apply-no-changes") 396 defer configCleanup() 397 398 op.Workspace = backend.DefaultStateName 399 400 run, err := b.Operation(context.Background(), op) 401 if err != nil { 402 t.Fatalf("error starting operation: %v", err) 403 } 404 405 <-run.Done() 406 if run.Result != backend.OperationSuccess { 407 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 408 } 409 if !run.PlanEmpty { 410 t.Fatalf("expected plan to be empty") 411 } 412 413 output := b.CLI.(*cli.MockUi).OutputWriter.String() 414 if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") { 415 t.Fatalf("expected no changes in plan summery: %s", output) 416 } 417 if !strings.Contains(output, "Sentinel Result: true") { 418 t.Fatalf("expected policy check result in output: %s", output) 419 } 420 } 421 422 func TestRemote_applyNoApprove(t *testing.T) { 423 b, bCleanup := testBackendDefault(t) 424 defer bCleanup() 425 426 op, configCleanup := testOperationApply(t, "./testdata/apply") 427 defer configCleanup() 428 429 input := testInput(t, map[string]string{ 430 "approve": "no", 431 }) 432 433 op.UIIn = input 434 op.UIOut = b.CLI 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 fail") 445 } 446 if !run.PlanEmpty { 447 t.Fatalf("expected plan to be empty") 448 } 449 450 if len(input.answers) > 0 { 451 t.Fatalf("expected no unused answers, got: %v", input.answers) 452 } 453 454 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 455 if !strings.Contains(errOutput, "Apply discarded") { 456 t.Fatalf("expected an apply discarded error, got: %v", errOutput) 457 } 458 } 459 460 func TestRemote_applyAutoApprove(t *testing.T) { 461 b, bCleanup := testBackendDefault(t) 462 defer bCleanup() 463 464 op, configCleanup := testOperationApply(t, "./testdata/apply") 465 defer configCleanup() 466 467 input := testInput(t, map[string]string{ 468 "approve": "no", 469 }) 470 471 op.AutoApprove = true 472 op.UIIn = input 473 op.UIOut = b.CLI 474 op.Workspace = backend.DefaultStateName 475 476 run, err := b.Operation(context.Background(), op) 477 if err != nil { 478 t.Fatalf("error starting operation: %v", err) 479 } 480 481 <-run.Done() 482 if run.Result != backend.OperationSuccess { 483 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 484 } 485 if run.PlanEmpty { 486 t.Fatalf("expected a non-empty plan") 487 } 488 489 if len(input.answers) != 1 { 490 t.Fatalf("expected an unused answer, got: %v", input.answers) 491 } 492 493 output := b.CLI.(*cli.MockUi).OutputWriter.String() 494 if !strings.Contains(output, "Running apply in the remote backend") { 495 t.Fatalf("expected remote backend header in output: %s", output) 496 } 497 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 498 t.Fatalf("expected plan summery in output: %s", output) 499 } 500 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 501 t.Fatalf("expected apply summery in output: %s", output) 502 } 503 } 504 505 func TestRemote_applyApprovedExternally(t *testing.T) { 506 b, bCleanup := testBackendDefault(t) 507 defer bCleanup() 508 509 op, configCleanup := testOperationApply(t, "./testdata/apply") 510 defer configCleanup() 511 512 input := testInput(t, map[string]string{ 513 "approve": "wait-for-external-update", 514 }) 515 516 op.UIIn = input 517 op.UIOut = b.CLI 518 op.Workspace = backend.DefaultStateName 519 520 ctx := context.Background() 521 522 run, err := b.Operation(ctx, op) 523 if err != nil { 524 t.Fatalf("error starting operation: %v", err) 525 } 526 527 // Wait 2 seconds to make sure the run started. 528 time.Sleep(2 * time.Second) 529 530 wl, err := b.client.Workspaces.List( 531 ctx, 532 b.organization, 533 tfe.WorkspaceListOptions{ 534 ListOptions: tfe.ListOptions{PageNumber: 2, PageSize: 10}, 535 }, 536 ) 537 if err != nil { 538 t.Fatalf("unexpected error listing workspaces: %v", err) 539 } 540 if len(wl.Items) != 1 { 541 t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items)) 542 } 543 544 rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, tfe.RunListOptions{}) 545 if err != nil { 546 t.Fatalf("unexpected error listing runs: %v", err) 547 } 548 if len(rl.Items) != 1 { 549 t.Fatalf("expected 1 run, got %d runs", len(rl.Items)) 550 } 551 552 err = b.client.Runs.Apply(context.Background(), rl.Items[0].ID, tfe.RunApplyOptions{}) 553 if err != nil { 554 t.Fatalf("unexpected error approving run: %v", err) 555 } 556 557 <-run.Done() 558 if run.Result != backend.OperationSuccess { 559 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 560 } 561 if run.PlanEmpty { 562 t.Fatalf("expected a non-empty plan") 563 } 564 565 output := b.CLI.(*cli.MockUi).OutputWriter.String() 566 if !strings.Contains(output, "Running apply in the remote backend") { 567 t.Fatalf("expected remote backend header in output: %s", output) 568 } 569 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 570 t.Fatalf("expected plan summery in output: %s", output) 571 } 572 if !strings.Contains(output, "approved using the UI or API") { 573 t.Fatalf("expected external approval in output: %s", output) 574 } 575 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 576 t.Fatalf("expected apply summery in output: %s", output) 577 } 578 } 579 580 func TestRemote_applyDiscardedExternally(t *testing.T) { 581 b, bCleanup := testBackendDefault(t) 582 defer bCleanup() 583 584 op, configCleanup := testOperationApply(t, "./testdata/apply") 585 defer configCleanup() 586 587 input := testInput(t, map[string]string{ 588 "approve": "wait-for-external-update", 589 }) 590 591 op.UIIn = input 592 op.UIOut = b.CLI 593 op.Workspace = backend.DefaultStateName 594 595 ctx := context.Background() 596 597 run, err := b.Operation(ctx, op) 598 if err != nil { 599 t.Fatalf("error starting operation: %v", err) 600 } 601 602 // Wait 2 seconds to make sure the run started. 603 time.Sleep(2 * time.Second) 604 605 wl, err := b.client.Workspaces.List( 606 ctx, 607 b.organization, 608 tfe.WorkspaceListOptions{ 609 ListOptions: tfe.ListOptions{PageNumber: 2, PageSize: 10}, 610 }, 611 ) 612 if err != nil { 613 t.Fatalf("unexpected error listing workspaces: %v", err) 614 } 615 if len(wl.Items) != 1 { 616 t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items)) 617 } 618 619 rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, tfe.RunListOptions{}) 620 if err != nil { 621 t.Fatalf("unexpected error listing runs: %v", err) 622 } 623 if len(rl.Items) != 1 { 624 t.Fatalf("expected 1 run, got %d runs", len(rl.Items)) 625 } 626 627 err = b.client.Runs.Discard(context.Background(), rl.Items[0].ID, tfe.RunDiscardOptions{}) 628 if err != nil { 629 t.Fatalf("unexpected error discarding run: %v", err) 630 } 631 632 <-run.Done() 633 if run.Result == backend.OperationSuccess { 634 t.Fatal("expected apply operation to fail") 635 } 636 if !run.PlanEmpty { 637 t.Fatalf("expected plan to be empty") 638 } 639 640 output := b.CLI.(*cli.MockUi).OutputWriter.String() 641 if !strings.Contains(output, "Running apply in the remote backend") { 642 t.Fatalf("expected remote backend header in output: %s", output) 643 } 644 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 645 t.Fatalf("expected plan summery in output: %s", output) 646 } 647 if !strings.Contains(output, "discarded using the UI or API") { 648 t.Fatalf("expected external discard output: %s", output) 649 } 650 if strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 651 t.Fatalf("unexpected apply summery in output: %s", output) 652 } 653 } 654 655 func TestRemote_applyWithAutoApply(t *testing.T) { 656 b, bCleanup := testBackendNoDefault(t) 657 defer bCleanup() 658 659 // Create a named workspace that auto applies. 660 _, err := b.client.Workspaces.Create( 661 context.Background(), 662 b.organization, 663 tfe.WorkspaceCreateOptions{ 664 AutoApply: tfe.Bool(true), 665 Name: tfe.String(b.prefix + "prod"), 666 }, 667 ) 668 if err != nil { 669 t.Fatalf("error creating named workspace: %v", err) 670 } 671 672 op, configCleanup := testOperationApply(t, "./testdata/apply") 673 defer configCleanup() 674 675 input := testInput(t, map[string]string{ 676 "approve": "yes", 677 }) 678 679 op.UIIn = input 680 op.UIOut = b.CLI 681 op.Workspace = "prod" 682 683 run, err := b.Operation(context.Background(), op) 684 if err != nil { 685 t.Fatalf("error starting operation: %v", err) 686 } 687 688 <-run.Done() 689 if run.Result != backend.OperationSuccess { 690 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 691 } 692 if run.PlanEmpty { 693 t.Fatalf("expected a non-empty plan") 694 } 695 696 if len(input.answers) != 1 { 697 t.Fatalf("expected an unused answer, got: %v", input.answers) 698 } 699 700 output := b.CLI.(*cli.MockUi).OutputWriter.String() 701 if !strings.Contains(output, "Running apply in the remote backend") { 702 t.Fatalf("expected remote backend header in output: %s", output) 703 } 704 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 705 t.Fatalf("expected plan summery in output: %s", output) 706 } 707 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 708 t.Fatalf("expected apply summery in output: %s", output) 709 } 710 } 711 712 func TestRemote_applyForceLocal(t *testing.T) { 713 // Set TF_FORCE_LOCAL_BACKEND so the remote backend will use 714 // the local backend with itself as embedded backend. 715 if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil { 716 t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err) 717 } 718 defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND") 719 720 b, bCleanup := testBackendDefault(t) 721 defer bCleanup() 722 723 op, configCleanup := testOperationApply(t, "./testdata/apply") 724 defer configCleanup() 725 726 input := testInput(t, map[string]string{ 727 "approve": "yes", 728 }) 729 730 op.UIIn = input 731 op.UIOut = b.CLI 732 op.Workspace = backend.DefaultStateName 733 734 run, err := b.Operation(context.Background(), op) 735 if err != nil { 736 t.Fatalf("error starting operation: %v", err) 737 } 738 739 <-run.Done() 740 if run.Result != backend.OperationSuccess { 741 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 742 } 743 if run.PlanEmpty { 744 t.Fatalf("expected a non-empty plan") 745 } 746 747 if len(input.answers) > 0 { 748 t.Fatalf("expected no unused answers, got: %v", input.answers) 749 } 750 751 output := b.CLI.(*cli.MockUi).OutputWriter.String() 752 if strings.Contains(output, "Running apply in the remote backend") { 753 t.Fatalf("unexpected remote backend header in output: %s", output) 754 } 755 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 756 t.Fatalf("expected plan summery in output: %s", output) 757 } 758 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 759 t.Fatalf("expected apply summery in output: %s", output) 760 } 761 } 762 763 func TestRemote_applyWorkspaceWithoutOperations(t *testing.T) { 764 b, bCleanup := testBackendNoDefault(t) 765 defer bCleanup() 766 767 ctx := context.Background() 768 769 // Create a named workspace that doesn't allow operations. 770 _, err := b.client.Workspaces.Create( 771 ctx, 772 b.organization, 773 tfe.WorkspaceCreateOptions{ 774 Name: tfe.String(b.prefix + "no-operations"), 775 }, 776 ) 777 if err != nil { 778 t.Fatalf("error creating named workspace: %v", err) 779 } 780 781 op, configCleanup := testOperationApply(t, "./testdata/apply") 782 defer configCleanup() 783 784 input := testInput(t, map[string]string{ 785 "approve": "yes", 786 }) 787 788 op.UIIn = input 789 op.UIOut = b.CLI 790 op.Workspace = "no-operations" 791 792 run, err := b.Operation(ctx, op) 793 if err != nil { 794 t.Fatalf("error starting operation: %v", err) 795 } 796 797 <-run.Done() 798 if run.Result != backend.OperationSuccess { 799 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 800 } 801 if run.PlanEmpty { 802 t.Fatalf("expected a non-empty plan") 803 } 804 805 if len(input.answers) > 0 { 806 t.Fatalf("expected no unused answers, got: %v", input.answers) 807 } 808 809 output := b.CLI.(*cli.MockUi).OutputWriter.String() 810 if strings.Contains(output, "Running apply in the remote backend") { 811 t.Fatalf("unexpected remote backend header in output: %s", output) 812 } 813 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 814 t.Fatalf("expected plan summery in output: %s", output) 815 } 816 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 817 t.Fatalf("expected apply summery in output: %s", output) 818 } 819 } 820 821 func TestRemote_applyLockTimeout(t *testing.T) { 822 b, bCleanup := testBackendDefault(t) 823 defer bCleanup() 824 825 ctx := context.Background() 826 827 // Retrieve the workspace used to run this operation in. 828 w, err := b.client.Workspaces.Read(ctx, b.organization, b.workspace) 829 if err != nil { 830 t.Fatalf("error retrieving workspace: %v", err) 831 } 832 833 // Create a new configuration version. 834 c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{}) 835 if err != nil { 836 t.Fatalf("error creating configuration version: %v", err) 837 } 838 839 // Create a pending run to block this run. 840 _, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{ 841 ConfigurationVersion: c, 842 Workspace: w, 843 }) 844 if err != nil { 845 t.Fatalf("error creating pending run: %v", err) 846 } 847 848 op, configCleanup := testOperationApply(t, "./testdata/apply") 849 defer configCleanup() 850 851 input := testInput(t, map[string]string{ 852 "cancel": "yes", 853 "approve": "yes", 854 }) 855 856 op.StateLockTimeout = 5 * time.Second 857 op.UIIn = input 858 op.UIOut = b.CLI 859 op.Workspace = backend.DefaultStateName 860 861 _, err = b.Operation(context.Background(), op) 862 if err != nil { 863 t.Fatalf("error starting operation: %v", err) 864 } 865 866 sigint := make(chan os.Signal, 1) 867 signal.Notify(sigint, syscall.SIGINT) 868 select { 869 case <-sigint: 870 // Stop redirecting SIGINT signals. 871 signal.Stop(sigint) 872 case <-time.After(10 * time.Second): 873 t.Fatalf("expected lock timeout after 5 seconds, waited 10 seconds") 874 } 875 876 if len(input.answers) != 2 { 877 t.Fatalf("expected unused answers, got: %v", input.answers) 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, "Lock timeout exceeded") { 885 t.Fatalf("expected lock timout error in output: %s", output) 886 } 887 if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 888 t.Fatalf("unexpected plan summery in 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_applyDestroy(t *testing.T) { 896 b, bCleanup := testBackendDefault(t) 897 defer bCleanup() 898 899 op, configCleanup := testOperationApply(t, "./testdata/apply-destroy") 900 defer configCleanup() 901 902 input := testInput(t, map[string]string{ 903 "approve": "yes", 904 }) 905 906 op.Destroy = true 907 op.UIIn = input 908 op.UIOut = b.CLI 909 op.Workspace = backend.DefaultStateName 910 911 run, err := b.Operation(context.Background(), op) 912 if err != nil { 913 t.Fatalf("error starting operation: %v", err) 914 } 915 916 <-run.Done() 917 if run.Result != backend.OperationSuccess { 918 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 919 } 920 if run.PlanEmpty { 921 t.Fatalf("expected a non-empty plan") 922 } 923 924 if len(input.answers) > 0 { 925 t.Fatalf("expected no unused answers, got: %v", input.answers) 926 } 927 928 output := b.CLI.(*cli.MockUi).OutputWriter.String() 929 if !strings.Contains(output, "Running apply in the remote backend") { 930 t.Fatalf("expected remote backend header in output: %s", output) 931 } 932 if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") { 933 t.Fatalf("expected plan summery in output: %s", output) 934 } 935 if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") { 936 t.Fatalf("expected apply summery in output: %s", output) 937 } 938 } 939 940 func TestRemote_applyDestroyNoConfig(t *testing.T) { 941 b, bCleanup := testBackendDefault(t) 942 defer bCleanup() 943 944 input := testInput(t, map[string]string{ 945 "approve": "yes", 946 }) 947 948 op, configCleanup := testOperationApply(t, "./testdata/empty") 949 defer configCleanup() 950 951 op.Destroy = true 952 op.UIIn = input 953 op.UIOut = b.CLI 954 op.Workspace = backend.DefaultStateName 955 956 run, err := b.Operation(context.Background(), op) 957 if err != nil { 958 t.Fatalf("error starting operation: %v", err) 959 } 960 961 <-run.Done() 962 if run.Result != backend.OperationSuccess { 963 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 964 } 965 if run.PlanEmpty { 966 t.Fatalf("expected a non-empty plan") 967 } 968 969 if len(input.answers) > 0 { 970 t.Fatalf("expected no unused answers, got: %v", input.answers) 971 } 972 } 973 974 func TestRemote_applyPolicyPass(t *testing.T) { 975 b, bCleanup := testBackendDefault(t) 976 defer bCleanup() 977 978 op, configCleanup := testOperationApply(t, "./testdata/apply-policy-passed") 979 defer configCleanup() 980 981 input := testInput(t, map[string]string{ 982 "approve": "yes", 983 }) 984 985 op.UIIn = input 986 op.UIOut = b.CLI 987 op.Workspace = backend.DefaultStateName 988 989 run, err := b.Operation(context.Background(), op) 990 if err != nil { 991 t.Fatalf("error starting operation: %v", err) 992 } 993 994 <-run.Done() 995 if run.Result != backend.OperationSuccess { 996 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 997 } 998 if run.PlanEmpty { 999 t.Fatalf("expected a non-empty plan") 1000 } 1001 1002 if len(input.answers) > 0 { 1003 t.Fatalf("expected no unused answers, got: %v", input.answers) 1004 } 1005 1006 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1007 if !strings.Contains(output, "Running apply in the remote backend") { 1008 t.Fatalf("expected remote backend header in output: %s", output) 1009 } 1010 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1011 t.Fatalf("expected plan summery in output: %s", output) 1012 } 1013 if !strings.Contains(output, "Sentinel Result: true") { 1014 t.Fatalf("expected policy check result in output: %s", output) 1015 } 1016 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1017 t.Fatalf("expected apply summery in output: %s", output) 1018 } 1019 } 1020 1021 func TestRemote_applyPolicyHardFail(t *testing.T) { 1022 b, bCleanup := testBackendDefault(t) 1023 defer bCleanup() 1024 1025 op, configCleanup := testOperationApply(t, "./testdata/apply-policy-hard-failed") 1026 defer configCleanup() 1027 1028 input := testInput(t, map[string]string{ 1029 "approve": "yes", 1030 }) 1031 1032 op.UIIn = input 1033 op.UIOut = b.CLI 1034 op.Workspace = backend.DefaultStateName 1035 1036 run, err := b.Operation(context.Background(), op) 1037 if err != nil { 1038 t.Fatalf("error starting operation: %v", err) 1039 } 1040 1041 <-run.Done() 1042 if run.Result == backend.OperationSuccess { 1043 t.Fatal("expected apply operation to fail") 1044 } 1045 if !run.PlanEmpty { 1046 t.Fatalf("expected plan to be empty") 1047 } 1048 1049 if len(input.answers) != 1 { 1050 t.Fatalf("expected an unused answers, got: %v", input.answers) 1051 } 1052 1053 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 1054 if !strings.Contains(errOutput, "hard failed") { 1055 t.Fatalf("expected a policy check error, got: %v", errOutput) 1056 } 1057 1058 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1059 if !strings.Contains(output, "Running apply in the remote backend") { 1060 t.Fatalf("expected remote backend header in output: %s", output) 1061 } 1062 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1063 t.Fatalf("expected plan summery in output: %s", output) 1064 } 1065 if !strings.Contains(output, "Sentinel Result: false") { 1066 t.Fatalf("expected policy check result in output: %s", output) 1067 } 1068 if strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1069 t.Fatalf("unexpected apply summery in output: %s", output) 1070 } 1071 } 1072 1073 func TestRemote_applyPolicySoftFail(t *testing.T) { 1074 b, bCleanup := testBackendDefault(t) 1075 defer bCleanup() 1076 1077 op, configCleanup := testOperationApply(t, "./testdata/apply-policy-soft-failed") 1078 defer configCleanup() 1079 1080 input := testInput(t, map[string]string{ 1081 "override": "override", 1082 "approve": "yes", 1083 }) 1084 1085 op.UIIn = input 1086 op.UIOut = b.CLI 1087 op.Workspace = backend.DefaultStateName 1088 1089 run, err := b.Operation(context.Background(), op) 1090 if err != nil { 1091 t.Fatalf("error starting operation: %v", err) 1092 } 1093 1094 <-run.Done() 1095 if run.Result != backend.OperationSuccess { 1096 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1097 } 1098 if run.PlanEmpty { 1099 t.Fatalf("expected a non-empty plan") 1100 } 1101 1102 if len(input.answers) > 0 { 1103 t.Fatalf("expected no unused answers, got: %v", input.answers) 1104 } 1105 1106 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1107 if !strings.Contains(output, "Running apply in the remote backend") { 1108 t.Fatalf("expected remote backend header in output: %s", output) 1109 } 1110 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1111 t.Fatalf("expected plan summery in output: %s", output) 1112 } 1113 if !strings.Contains(output, "Sentinel Result: false") { 1114 t.Fatalf("expected policy check result in output: %s", output) 1115 } 1116 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1117 t.Fatalf("expected apply summery in output: %s", output) 1118 } 1119 } 1120 1121 func TestRemote_applyPolicySoftFailAutoApprove(t *testing.T) { 1122 b, bCleanup := testBackendDefault(t) 1123 defer bCleanup() 1124 1125 op, configCleanup := testOperationApply(t, "./testdata/apply-policy-soft-failed") 1126 defer configCleanup() 1127 1128 input := testInput(t, map[string]string{ 1129 "override": "override", 1130 }) 1131 1132 op.AutoApprove = true 1133 op.UIIn = input 1134 op.UIOut = b.CLI 1135 op.Workspace = backend.DefaultStateName 1136 1137 run, err := b.Operation(context.Background(), op) 1138 if err != nil { 1139 t.Fatalf("error starting operation: %v", err) 1140 } 1141 1142 <-run.Done() 1143 if run.Result == backend.OperationSuccess { 1144 t.Fatal("expected apply operation to fail") 1145 } 1146 if !run.PlanEmpty { 1147 t.Fatalf("expected plan to be empty") 1148 } 1149 1150 if len(input.answers) != 1 { 1151 t.Fatalf("expected an unused answers, got: %v", input.answers) 1152 } 1153 1154 errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() 1155 if !strings.Contains(errOutput, "soft failed") { 1156 t.Fatalf("expected a policy check error, got: %v", errOutput) 1157 } 1158 1159 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1160 if !strings.Contains(output, "Running apply in the remote backend") { 1161 t.Fatalf("expected remote backend header in output: %s", output) 1162 } 1163 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1164 t.Fatalf("expected plan summery in output: %s", output) 1165 } 1166 if !strings.Contains(output, "Sentinel Result: false") { 1167 t.Fatalf("expected policy check result in output: %s", output) 1168 } 1169 if strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1170 t.Fatalf("unexpected apply summery in output: %s", output) 1171 } 1172 } 1173 1174 func TestRemote_applyPolicySoftFailAutoApply(t *testing.T) { 1175 b, bCleanup := testBackendDefault(t) 1176 defer bCleanup() 1177 1178 // Create a named workspace that auto applies. 1179 _, err := b.client.Workspaces.Create( 1180 context.Background(), 1181 b.organization, 1182 tfe.WorkspaceCreateOptions{ 1183 AutoApply: tfe.Bool(true), 1184 Name: tfe.String(b.prefix + "prod"), 1185 }, 1186 ) 1187 if err != nil { 1188 t.Fatalf("error creating named workspace: %v", err) 1189 } 1190 1191 op, configCleanup := testOperationApply(t, "./testdata/apply-policy-soft-failed") 1192 defer configCleanup() 1193 1194 input := testInput(t, map[string]string{ 1195 "override": "override", 1196 "approve": "yes", 1197 }) 1198 1199 op.UIIn = input 1200 op.UIOut = b.CLI 1201 op.Workspace = "prod" 1202 1203 run, err := b.Operation(context.Background(), op) 1204 if err != nil { 1205 t.Fatalf("error starting operation: %v", err) 1206 } 1207 1208 <-run.Done() 1209 if run.Result != backend.OperationSuccess { 1210 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1211 } 1212 if run.PlanEmpty { 1213 t.Fatalf("expected a non-empty plan") 1214 } 1215 1216 if len(input.answers) != 1 { 1217 t.Fatalf("expected an unused answer, got: %v", input.answers) 1218 } 1219 1220 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1221 if !strings.Contains(output, "Running apply in the remote backend") { 1222 t.Fatalf("expected remote backend header in output: %s", output) 1223 } 1224 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1225 t.Fatalf("expected plan summery in output: %s", output) 1226 } 1227 if !strings.Contains(output, "Sentinel Result: false") { 1228 t.Fatalf("expected policy check result in output: %s", output) 1229 } 1230 if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { 1231 t.Fatalf("expected apply summery in output: %s", output) 1232 } 1233 } 1234 1235 func TestRemote_applyWithRemoteError(t *testing.T) { 1236 b, bCleanup := testBackendDefault(t) 1237 defer bCleanup() 1238 1239 op, configCleanup := testOperationApply(t, "./testdata/apply-with-error") 1240 defer configCleanup() 1241 1242 op.Workspace = backend.DefaultStateName 1243 1244 run, err := b.Operation(context.Background(), op) 1245 if err != nil { 1246 t.Fatalf("error starting operation: %v", err) 1247 } 1248 1249 <-run.Done() 1250 if run.Result == backend.OperationSuccess { 1251 t.Fatal("expected apply operation to fail") 1252 } 1253 if run.Result.ExitStatus() != 1 { 1254 t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) 1255 } 1256 1257 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1258 if !strings.Contains(output, "null_resource.foo: 1 error") { 1259 t.Fatalf("expected apply error in output: %s", output) 1260 } 1261 }