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