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