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