github.com/pulumi/terraform@v1.4.0/pkg/cloud/backend_plan_test.go (about) 1 package cloud 2 3 import ( 4 "context" 5 "os" 6 "os/signal" 7 "strings" 8 "syscall" 9 "testing" 10 "time" 11 12 "github.com/google/go-cmp/cmp" 13 tfe "github.com/hashicorp/go-tfe" 14 "github.com/pulumi/terraform/pkg/addrs" 15 "github.com/pulumi/terraform/pkg/backend" 16 "github.com/pulumi/terraform/pkg/command/arguments" 17 "github.com/pulumi/terraform/pkg/command/clistate" 18 "github.com/pulumi/terraform/pkg/command/jsonformat" 19 "github.com/pulumi/terraform/pkg/command/views" 20 "github.com/pulumi/terraform/pkg/depsfile" 21 "github.com/pulumi/terraform/pkg/initwd" 22 "github.com/pulumi/terraform/pkg/plans" 23 "github.com/pulumi/terraform/pkg/plans/planfile" 24 "github.com/pulumi/terraform/pkg/states/statemgr" 25 "github.com/pulumi/terraform/pkg/terminal" 26 "github.com/pulumi/terraform/pkg/terraform" 27 "github.com/mitchellh/cli" 28 ) 29 30 func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 31 t.Helper() 32 33 return testOperationPlanWithTimeout(t, configDir, 0) 34 } 35 36 func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { 37 t.Helper() 38 39 _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) 40 41 streams, done := terminal.StreamsForTesting(t) 42 view := views.NewView(streams) 43 stateLockerView := views.NewStateLocker(arguments.ViewHuman, view) 44 operationView := views.NewOperation(arguments.ViewHuman, false, view) 45 46 // Many of our tests use an overridden "null" provider that's just in-memory 47 // inside the test process, not a separate plugin on disk. 48 depLocks := depsfile.NewLocks() 49 depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")) 50 51 return &backend.Operation{ 52 ConfigDir: configDir, 53 ConfigLoader: configLoader, 54 PlanRefresh: true, 55 StateLocker: clistate.NewLocker(timeout, stateLockerView), 56 Type: backend.OperationTypePlan, 57 View: operationView, 58 DependencyLocks: depLocks, 59 }, configCleanup, done 60 } 61 62 func TestCloud_planBasic(t *testing.T) { 63 b, bCleanup := testBackendWithName(t) 64 defer bCleanup() 65 66 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 67 defer configCleanup() 68 defer done(t) 69 70 op.Workspace = testBackendSingleWorkspaceName 71 72 run, err := b.Operation(context.Background(), op) 73 if err != nil { 74 t.Fatalf("error starting operation: %v", err) 75 } 76 77 <-run.Done() 78 if run.Result != backend.OperationSuccess { 79 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 80 } 81 if run.PlanEmpty { 82 t.Fatal("expected a non-empty plan") 83 } 84 85 output := b.CLI.(*cli.MockUi).OutputWriter.String() 86 if !strings.Contains(output, "Running plan in Terraform Cloud") { 87 t.Fatalf("expected TFC header in output: %s", output) 88 } 89 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 90 t.Fatalf("expected plan summary in output: %s", output) 91 } 92 93 stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) 94 // An error suggests that the state was not unlocked after the operation finished 95 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 96 t.Fatalf("unexpected error locking state after successful plan: %s", err.Error()) 97 } 98 } 99 100 func TestCloud_planJSONBasic(t *testing.T) { 101 b, bCleanup := testBackendWithName(t) 102 defer bCleanup() 103 104 stream, close := terminal.StreamsForTesting(t) 105 106 b.renderer = &jsonformat.Renderer{ 107 Streams: stream, 108 Colorize: mockColorize(), 109 } 110 111 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-json-basic") 112 defer configCleanup() 113 defer done(t) 114 115 op.Workspace = testBackendSingleWorkspaceName 116 117 mockSROWorkspace(t, b, op.Workspace) 118 119 run, err := b.Operation(context.Background(), op) 120 if err != nil { 121 t.Fatalf("error starting operation: %v", err) 122 } 123 124 <-run.Done() 125 if run.Result != backend.OperationSuccess { 126 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 127 } 128 if run.PlanEmpty { 129 t.Fatal("expected a non-empty plan") 130 } 131 132 outp := close(t) 133 gotOut := outp.Stdout() 134 135 if !strings.Contains(gotOut, "1 to add, 0 to change, 0 to destroy") { 136 t.Fatalf("expected plan summary in output: %s", gotOut) 137 } 138 139 stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) 140 // An error suggests that the state was not unlocked after the operation finished 141 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 142 t.Fatalf("unexpected error locking state after successful plan: %s", err.Error()) 143 } 144 } 145 146 func TestCloud_planCanceled(t *testing.T) { 147 b, bCleanup := testBackendWithName(t) 148 defer bCleanup() 149 150 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 151 defer configCleanup() 152 defer done(t) 153 154 op.Workspace = testBackendSingleWorkspaceName 155 156 run, err := b.Operation(context.Background(), op) 157 if err != nil { 158 t.Fatalf("error starting operation: %v", err) 159 } 160 161 // Stop the run to simulate a Ctrl-C. 162 run.Stop() 163 164 <-run.Done() 165 if run.Result == backend.OperationSuccess { 166 t.Fatal("expected plan operation to fail") 167 } 168 169 stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) 170 // An error suggests that the state was not unlocked after the operation finished 171 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 172 t.Fatalf("unexpected error locking state after cancelled plan: %s", err.Error()) 173 } 174 } 175 176 func TestCloud_planLongLine(t *testing.T) { 177 b, bCleanup := testBackendWithName(t) 178 defer bCleanup() 179 180 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-long-line") 181 defer configCleanup() 182 defer done(t) 183 184 op.Workspace = testBackendSingleWorkspaceName 185 186 run, err := b.Operation(context.Background(), op) 187 if err != nil { 188 t.Fatalf("error starting operation: %v", err) 189 } 190 191 <-run.Done() 192 if run.Result != backend.OperationSuccess { 193 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 194 } 195 if run.PlanEmpty { 196 t.Fatal("expected a non-empty plan") 197 } 198 199 output := b.CLI.(*cli.MockUi).OutputWriter.String() 200 if !strings.Contains(output, "Running plan in Terraform Cloud") { 201 t.Fatalf("expected TFC header in output: %s", output) 202 } 203 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 204 t.Fatalf("expected plan summary in output: %s", output) 205 } 206 } 207 208 func TestCloud_planJSONFull(t *testing.T) { 209 b, bCleanup := testBackendWithName(t) 210 defer bCleanup() 211 212 stream, close := terminal.StreamsForTesting(t) 213 214 b.renderer = &jsonformat.Renderer{ 215 Streams: stream, 216 Colorize: mockColorize(), 217 } 218 219 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-json-full") 220 defer configCleanup() 221 defer done(t) 222 223 op.Workspace = testBackendSingleWorkspaceName 224 225 mockSROWorkspace(t, b, op.Workspace) 226 227 run, err := b.Operation(context.Background(), op) 228 if err != nil { 229 t.Fatalf("error starting operation: %v", err) 230 } 231 232 <-run.Done() 233 if run.Result != backend.OperationSuccess { 234 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 235 } 236 if run.PlanEmpty { 237 t.Fatal("expected a non-empty plan") 238 } 239 240 outp := close(t) 241 gotOut := outp.Stdout() 242 243 if !strings.Contains(gotOut, "tfcoremock_simple_resource.example: Refreshing state... [id=my-simple-resource]") { 244 t.Fatalf("expected plan log: %s", gotOut) 245 } 246 247 if !strings.Contains(gotOut, "2 to add, 0 to change, 0 to destroy") { 248 t.Fatalf("expected plan summary in output: %s", gotOut) 249 } 250 251 stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) 252 // An error suggests that the state was not unlocked after the operation finished 253 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 254 t.Fatalf("unexpected error locking state after successful plan: %s", err.Error()) 255 } 256 } 257 258 func TestCloud_planWithoutPermissions(t *testing.T) { 259 b, bCleanup := testBackendWithTags(t) 260 defer bCleanup() 261 262 // Create a named workspace without permissions. 263 w, err := b.client.Workspaces.Create( 264 context.Background(), 265 b.organization, 266 tfe.WorkspaceCreateOptions{ 267 Name: tfe.String("prod"), 268 }, 269 ) 270 if err != nil { 271 t.Fatalf("error creating named workspace: %v", err) 272 } 273 w.Permissions.CanQueueRun = false 274 275 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 276 defer configCleanup() 277 278 op.Workspace = "prod" 279 280 run, err := b.Operation(context.Background(), op) 281 if err != nil { 282 t.Fatalf("error starting operation: %v", err) 283 } 284 285 <-run.Done() 286 output := done(t) 287 if run.Result == backend.OperationSuccess { 288 t.Fatal("expected plan operation to fail") 289 } 290 291 errOutput := output.Stderr() 292 if !strings.Contains(errOutput, "Insufficient rights to generate a plan") { 293 t.Fatalf("expected a permissions error, got: %v", errOutput) 294 } 295 } 296 297 func TestCloud_planWithParallelism(t *testing.T) { 298 b, bCleanup := testBackendWithName(t) 299 defer bCleanup() 300 301 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 302 defer configCleanup() 303 304 if b.ContextOpts == nil { 305 b.ContextOpts = &terraform.ContextOpts{} 306 } 307 b.ContextOpts.Parallelism = 3 308 op.Workspace = testBackendSingleWorkspaceName 309 310 run, err := b.Operation(context.Background(), op) 311 if err != nil { 312 t.Fatalf("error starting operation: %v", err) 313 } 314 315 <-run.Done() 316 output := done(t) 317 if run.Result == backend.OperationSuccess { 318 t.Fatal("expected plan operation to fail") 319 } 320 321 errOutput := output.Stderr() 322 if !strings.Contains(errOutput, "parallelism values are currently not supported") { 323 t.Fatalf("expected a parallelism error, got: %v", errOutput) 324 } 325 } 326 327 func TestCloud_planWithPlan(t *testing.T) { 328 b, bCleanup := testBackendWithName(t) 329 defer bCleanup() 330 331 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 332 defer configCleanup() 333 334 op.PlanFile = &planfile.Reader{} 335 op.Workspace = testBackendSingleWorkspaceName 336 337 run, err := b.Operation(context.Background(), op) 338 if err != nil { 339 t.Fatalf("error starting operation: %v", err) 340 } 341 342 <-run.Done() 343 output := done(t) 344 if run.Result == backend.OperationSuccess { 345 t.Fatal("expected plan operation to fail") 346 } 347 if !run.PlanEmpty { 348 t.Fatalf("expected plan to be empty") 349 } 350 351 errOutput := output.Stderr() 352 if !strings.Contains(errOutput, "saved plan is currently not supported") { 353 t.Fatalf("expected a saved plan error, got: %v", errOutput) 354 } 355 } 356 357 func TestCloud_planWithPath(t *testing.T) { 358 b, bCleanup := testBackendWithName(t) 359 defer bCleanup() 360 361 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 362 defer configCleanup() 363 364 op.PlanOutPath = "./testdata/plan" 365 op.Workspace = testBackendSingleWorkspaceName 366 367 run, err := b.Operation(context.Background(), op) 368 if err != nil { 369 t.Fatalf("error starting operation: %v", err) 370 } 371 372 <-run.Done() 373 output := done(t) 374 if run.Result == backend.OperationSuccess { 375 t.Fatal("expected plan operation to fail") 376 } 377 if !run.PlanEmpty { 378 t.Fatalf("expected plan to be empty") 379 } 380 381 errOutput := output.Stderr() 382 if !strings.Contains(errOutput, "generated plan is currently not supported") { 383 t.Fatalf("expected a generated plan error, got: %v", errOutput) 384 } 385 } 386 387 func TestCloud_planWithoutRefresh(t *testing.T) { 388 b, bCleanup := testBackendWithName(t) 389 defer bCleanup() 390 391 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 392 defer configCleanup() 393 defer done(t) 394 395 op.PlanRefresh = false 396 op.Workspace = testBackendSingleWorkspaceName 397 398 run, err := b.Operation(context.Background(), op) 399 if err != nil { 400 t.Fatalf("error starting operation: %v", err) 401 } 402 403 <-run.Done() 404 if run.Result != backend.OperationSuccess { 405 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 406 } 407 if run.PlanEmpty { 408 t.Fatal("expected a non-empty plan") 409 } 410 411 // We should find a run inside the mock client that has refresh set 412 // to false. 413 runsAPI := b.client.Runs.(*MockRuns) 414 if got, want := len(runsAPI.Runs), 1; got != want { 415 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 416 } 417 for _, run := range runsAPI.Runs { 418 if diff := cmp.Diff(false, run.Refresh); diff != "" { 419 t.Errorf("wrong Refresh setting in the created run\n%s", diff) 420 } 421 } 422 } 423 424 func TestCloud_planWithRefreshOnly(t *testing.T) { 425 b, bCleanup := testBackendWithName(t) 426 defer bCleanup() 427 428 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 429 defer configCleanup() 430 defer done(t) 431 432 op.PlanMode = plans.RefreshOnlyMode 433 op.Workspace = testBackendSingleWorkspaceName 434 435 run, err := b.Operation(context.Background(), op) 436 if err != nil { 437 t.Fatalf("error starting operation: %v", err) 438 } 439 440 <-run.Done() 441 if run.Result != backend.OperationSuccess { 442 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 443 } 444 if run.PlanEmpty { 445 t.Fatal("expected a non-empty plan") 446 } 447 448 // We should find a run inside the mock client that has refresh-only set 449 // to true. 450 runsAPI := b.client.Runs.(*MockRuns) 451 if got, want := len(runsAPI.Runs), 1; got != want { 452 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 453 } 454 for _, run := range runsAPI.Runs { 455 if diff := cmp.Diff(true, run.RefreshOnly); diff != "" { 456 t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff) 457 } 458 } 459 } 460 461 func TestCloud_planWithTarget(t *testing.T) { 462 b, bCleanup := testBackendWithName(t) 463 defer bCleanup() 464 465 // When the backend code creates a new run, we'll tweak it so that it 466 // has a cost estimation object with the "skipped_due_to_targeting" status, 467 // emulating how a real server is expected to behave in that case. 468 b.client.Runs.(*MockRuns).ModifyNewRun = func(client *MockClient, options tfe.RunCreateOptions, run *tfe.Run) { 469 const fakeID = "fake" 470 // This is the cost estimate object embedded in the run itself which 471 // the backend will use to learn the ID to request from the cost 472 // estimates endpoint. It's pending to simulate what a freshly-created 473 // run is likely to look like. 474 run.CostEstimate = &tfe.CostEstimate{ 475 ID: fakeID, 476 Status: "pending", 477 } 478 // The backend will then use the main cost estimation API to retrieve 479 // the same ID indicated in the object above, where we'll then return 480 // the status "skipped_due_to_targeting" to trigger the special skip 481 // message in the backend output. 482 client.CostEstimates.Estimations[fakeID] = &tfe.CostEstimate{ 483 ID: fakeID, 484 Status: "skipped_due_to_targeting", 485 } 486 } 487 488 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 489 defer configCleanup() 490 defer done(t) 491 492 addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") 493 494 op.Targets = []addrs.Targetable{addr} 495 op.Workspace = testBackendSingleWorkspaceName 496 497 run, err := b.Operation(context.Background(), op) 498 if err != nil { 499 t.Fatalf("error starting operation: %v", err) 500 } 501 502 <-run.Done() 503 if run.Result != backend.OperationSuccess { 504 t.Fatal("expected plan operation to succeed") 505 } 506 if run.PlanEmpty { 507 t.Fatalf("expected plan to be non-empty") 508 } 509 510 // testBackendDefault above attached a "mock UI" to our backend, so we 511 // can retrieve its non-error output via the OutputWriter in-memory buffer. 512 gotOutput := b.CLI.(*cli.MockUi).OutputWriter.String() 513 if wantOutput := "Not available for this plan, because it was created with the -target option."; !strings.Contains(gotOutput, wantOutput) { 514 t.Errorf("missing message about skipped cost estimation\ngot:\n%s\nwant substring: %s", gotOutput, wantOutput) 515 } 516 517 // We should find a run inside the mock client that has the same 518 // target address we requested above. 519 runsAPI := b.client.Runs.(*MockRuns) 520 if got, want := len(runsAPI.Runs), 1; got != want { 521 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 522 } 523 for _, run := range runsAPI.Runs { 524 if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { 525 t.Errorf("wrong TargetAddrs in the created run\n%s", diff) 526 } 527 } 528 } 529 530 func TestCloud_planWithReplace(t *testing.T) { 531 b, bCleanup := testBackendWithName(t) 532 defer bCleanup() 533 534 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 535 defer configCleanup() 536 defer done(t) 537 538 addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") 539 540 op.ForceReplace = []addrs.AbsResourceInstance{addr} 541 op.Workspace = testBackendSingleWorkspaceName 542 543 run, err := b.Operation(context.Background(), op) 544 if err != nil { 545 t.Fatalf("error starting operation: %v", err) 546 } 547 548 <-run.Done() 549 if run.Result != backend.OperationSuccess { 550 t.Fatal("expected plan operation to succeed") 551 } 552 if run.PlanEmpty { 553 t.Fatalf("expected plan to be non-empty") 554 } 555 556 // We should find a run inside the mock client that has the same 557 // refresh address we requested above. 558 runsAPI := b.client.Runs.(*MockRuns) 559 if got, want := len(runsAPI.Runs), 1; got != want { 560 t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) 561 } 562 for _, run := range runsAPI.Runs { 563 if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" { 564 t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff) 565 } 566 } 567 } 568 569 func TestCloud_planWithRequiredVariables(t *testing.T) { 570 b, bCleanup := testBackendWithName(t) 571 defer bCleanup() 572 573 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-variables") 574 defer configCleanup() 575 defer done(t) 576 577 op.Variables = testVariables(terraform.ValueFromCLIArg, "foo") // "bar" variable defined in config is missing 578 op.Workspace = testBackendSingleWorkspaceName 579 580 run, err := b.Operation(context.Background(), op) 581 if err != nil { 582 t.Fatalf("error starting operation: %v", err) 583 } 584 585 <-run.Done() 586 // The usual error of a required variable being missing is deferred and the operation 587 // is successful. 588 if run.Result != backend.OperationSuccess { 589 t.Fatal("expected plan operation to succeed") 590 } 591 592 output := b.CLI.(*cli.MockUi).OutputWriter.String() 593 if !strings.Contains(output, "Running plan in Terraform Cloud") { 594 t.Fatalf("unexpected TFC header in output: %s", output) 595 } 596 } 597 598 func TestCloud_planNoConfig(t *testing.T) { 599 b, bCleanup := testBackendWithName(t) 600 defer bCleanup() 601 602 op, configCleanup, done := testOperationPlan(t, "./testdata/empty") 603 defer configCleanup() 604 605 op.Workspace = testBackendSingleWorkspaceName 606 607 run, err := b.Operation(context.Background(), op) 608 if err != nil { 609 t.Fatalf("error starting operation: %v", err) 610 } 611 612 <-run.Done() 613 output := done(t) 614 if run.Result == backend.OperationSuccess { 615 t.Fatal("expected plan operation to fail") 616 } 617 if !run.PlanEmpty { 618 t.Fatalf("expected plan to be empty") 619 } 620 621 errOutput := output.Stderr() 622 if !strings.Contains(errOutput, "configuration files found") { 623 t.Fatalf("expected configuration files error, got: %v", errOutput) 624 } 625 } 626 627 func TestCloud_planNoChanges(t *testing.T) { 628 b, bCleanup := testBackendWithName(t) 629 defer bCleanup() 630 631 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-no-changes") 632 defer configCleanup() 633 defer done(t) 634 635 op.Workspace = testBackendSingleWorkspaceName 636 637 run, err := b.Operation(context.Background(), op) 638 if err != nil { 639 t.Fatalf("error starting operation: %v", err) 640 } 641 642 <-run.Done() 643 if run.Result != backend.OperationSuccess { 644 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 645 } 646 if !run.PlanEmpty { 647 t.Fatalf("expected plan to be empty") 648 } 649 650 output := b.CLI.(*cli.MockUi).OutputWriter.String() 651 if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") { 652 t.Fatalf("expected no changes in plan summary: %s", output) 653 } 654 if !strings.Contains(output, "Sentinel Result: true") { 655 t.Fatalf("expected policy check result in output: %s", output) 656 } 657 } 658 659 func TestCloud_planForceLocal(t *testing.T) { 660 // Set TF_FORCE_LOCAL_BACKEND so the cloud backend will use 661 // the local backend with itself as embedded backend. 662 if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil { 663 t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err) 664 } 665 defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND") 666 667 b, bCleanup := testBackendWithName(t) 668 defer bCleanup() 669 670 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 671 defer configCleanup() 672 defer done(t) 673 674 op.Workspace = testBackendSingleWorkspaceName 675 676 streams, done := terminal.StreamsForTesting(t) 677 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 678 op.View = view 679 680 run, err := b.Operation(context.Background(), op) 681 if err != nil { 682 t.Fatalf("error starting operation: %v", err) 683 } 684 685 <-run.Done() 686 if run.Result != backend.OperationSuccess { 687 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 688 } 689 if run.PlanEmpty { 690 t.Fatalf("expected a non-empty plan") 691 } 692 693 output := b.CLI.(*cli.MockUi).OutputWriter.String() 694 if strings.Contains(output, "Running plan in Terraform Cloud") { 695 t.Fatalf("unexpected TFC header in output: %s", output) 696 } 697 if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 698 t.Fatalf("expected plan summary in output: %s", output) 699 } 700 } 701 702 func TestCloud_planWithoutOperationsEntitlement(t *testing.T) { 703 b, bCleanup := testBackendNoOperations(t) 704 defer bCleanup() 705 706 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 707 defer configCleanup() 708 defer done(t) 709 710 op.Workspace = testBackendSingleWorkspaceName 711 712 streams, done := terminal.StreamsForTesting(t) 713 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 714 op.View = view 715 716 run, err := b.Operation(context.Background(), op) 717 if err != nil { 718 t.Fatalf("error starting operation: %v", err) 719 } 720 721 <-run.Done() 722 if run.Result != backend.OperationSuccess { 723 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 724 } 725 if run.PlanEmpty { 726 t.Fatalf("expected a non-empty plan") 727 } 728 729 output := b.CLI.(*cli.MockUi).OutputWriter.String() 730 if strings.Contains(output, "Running plan in Terraform Cloud") { 731 t.Fatalf("unexpected TFC header in output: %s", output) 732 } 733 if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 734 t.Fatalf("expected plan summary in output: %s", output) 735 } 736 } 737 738 func TestCloud_planWorkspaceWithoutOperations(t *testing.T) { 739 b, bCleanup := testBackendWithTags(t) 740 defer bCleanup() 741 742 ctx := context.Background() 743 744 // Create a named workspace that doesn't allow operations. 745 _, err := b.client.Workspaces.Create( 746 ctx, 747 b.organization, 748 tfe.WorkspaceCreateOptions{ 749 Name: tfe.String("no-operations"), 750 }, 751 ) 752 if err != nil { 753 t.Fatalf("error creating named workspace: %v", err) 754 } 755 756 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 757 defer configCleanup() 758 defer done(t) 759 760 op.Workspace = "no-operations" 761 762 streams, done := terminal.StreamsForTesting(t) 763 view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) 764 op.View = view 765 766 run, err := b.Operation(ctx, op) 767 if err != nil { 768 t.Fatalf("error starting operation: %v", err) 769 } 770 771 <-run.Done() 772 if run.Result != backend.OperationSuccess { 773 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 774 } 775 if run.PlanEmpty { 776 t.Fatalf("expected a non-empty plan") 777 } 778 779 output := b.CLI.(*cli.MockUi).OutputWriter.String() 780 if strings.Contains(output, "Running plan in Terraform Cloud") { 781 t.Fatalf("unexpected TFC header in output: %s", output) 782 } 783 if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 784 t.Fatalf("expected plan summary in output: %s", output) 785 } 786 } 787 788 func TestCloud_planLockTimeout(t *testing.T) { 789 b, bCleanup := testBackendWithName(t) 790 defer bCleanup() 791 792 ctx := context.Background() 793 794 // Retrieve the workspace used to run this operation in. 795 w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name) 796 if err != nil { 797 t.Fatalf("error retrieving workspace: %v", err) 798 } 799 800 // Create a new configuration version. 801 c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{}) 802 if err != nil { 803 t.Fatalf("error creating configuration version: %v", err) 804 } 805 806 // Create a pending run to block this run. 807 _, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{ 808 ConfigurationVersion: c, 809 Workspace: w, 810 }) 811 if err != nil { 812 t.Fatalf("error creating pending run: %v", err) 813 } 814 815 op, configCleanup, done := testOperationPlanWithTimeout(t, "./testdata/plan", 50) 816 defer configCleanup() 817 defer done(t) 818 819 input := testInput(t, map[string]string{ 820 "cancel": "yes", 821 "approve": "yes", 822 }) 823 824 op.UIIn = input 825 op.UIOut = b.CLI 826 op.Workspace = testBackendSingleWorkspaceName 827 828 _, err = b.Operation(context.Background(), op) 829 if err != nil { 830 t.Fatalf("error starting operation: %v", err) 831 } 832 833 sigint := make(chan os.Signal, 1) 834 signal.Notify(sigint, syscall.SIGINT) 835 select { 836 case <-sigint: 837 // Stop redirecting SIGINT signals. 838 signal.Stop(sigint) 839 case <-time.After(200 * time.Millisecond): 840 t.Fatalf("expected lock timeout after 50 milliseconds, waited 200 milliseconds") 841 } 842 843 if len(input.answers) != 2 { 844 t.Fatalf("expected unused answers, got: %v", input.answers) 845 } 846 847 output := b.CLI.(*cli.MockUi).OutputWriter.String() 848 if !strings.Contains(output, "Running plan in Terraform Cloud") { 849 t.Fatalf("expected TFC header in output: %s", output) 850 } 851 if !strings.Contains(output, "Lock timeout exceeded") { 852 t.Fatalf("expected lock timout error in output: %s", output) 853 } 854 if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 855 t.Fatalf("unexpected plan summary in output: %s", output) 856 } 857 } 858 859 func TestCloud_planDestroy(t *testing.T) { 860 b, bCleanup := testBackendWithName(t) 861 defer bCleanup() 862 863 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 864 defer configCleanup() 865 defer done(t) 866 867 op.PlanMode = plans.DestroyMode 868 op.Workspace = testBackendSingleWorkspaceName 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 884 func TestCloud_planDestroyNoConfig(t *testing.T) { 885 b, bCleanup := testBackendWithName(t) 886 defer bCleanup() 887 888 op, configCleanup, done := testOperationPlan(t, "./testdata/empty") 889 defer configCleanup() 890 defer done(t) 891 892 op.PlanMode = plans.DestroyMode 893 op.Workspace = testBackendSingleWorkspaceName 894 895 run, err := b.Operation(context.Background(), op) 896 if err != nil { 897 t.Fatalf("error starting operation: %v", err) 898 } 899 900 <-run.Done() 901 if run.Result != backend.OperationSuccess { 902 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 903 } 904 if run.PlanEmpty { 905 t.Fatalf("expected a non-empty plan") 906 } 907 } 908 909 func TestCloud_planWithWorkingDirectory(t *testing.T) { 910 b, bCleanup := testBackendWithName(t) 911 defer bCleanup() 912 913 options := tfe.WorkspaceUpdateOptions{ 914 WorkingDirectory: tfe.String("terraform"), 915 } 916 917 // Configure the workspace to use a custom working directory. 918 _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.WorkspaceMapping.Name, options) 919 if err != nil { 920 t.Fatalf("error configuring working directory: %v", err) 921 } 922 923 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-working-directory/terraform") 924 defer configCleanup() 925 defer done(t) 926 927 op.Workspace = testBackendSingleWorkspaceName 928 929 run, err := b.Operation(context.Background(), op) 930 if err != nil { 931 t.Fatalf("error starting operation: %v", err) 932 } 933 934 <-run.Done() 935 if run.Result != backend.OperationSuccess { 936 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 937 } 938 if run.PlanEmpty { 939 t.Fatalf("expected a non-empty plan") 940 } 941 942 output := b.CLI.(*cli.MockUi).OutputWriter.String() 943 if !strings.Contains(output, "The remote workspace is configured to work with configuration") { 944 t.Fatalf("expected working directory warning: %s", output) 945 } 946 if !strings.Contains(output, "Running plan in Terraform Cloud") { 947 t.Fatalf("expected TFC header in output: %s", output) 948 } 949 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 950 t.Fatalf("expected plan summary in output: %s", output) 951 } 952 } 953 954 func TestCloud_planWithWorkingDirectoryFromCurrentPath(t *testing.T) { 955 b, bCleanup := testBackendWithName(t) 956 defer bCleanup() 957 958 options := tfe.WorkspaceUpdateOptions{ 959 WorkingDirectory: tfe.String("terraform"), 960 } 961 962 // Configure the workspace to use a custom working directory. 963 _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.WorkspaceMapping.Name, options) 964 if err != nil { 965 t.Fatalf("error configuring working directory: %v", err) 966 } 967 968 wd, err := os.Getwd() 969 if err != nil { 970 t.Fatalf("error getting current working directory: %v", err) 971 } 972 973 // We need to change into the configuration directory to make sure 974 // the logic to upload the correct slug is working as expected. 975 if err := os.Chdir("./testdata/plan-with-working-directory/terraform"); err != nil { 976 t.Fatalf("error changing directory: %v", err) 977 } 978 defer os.Chdir(wd) // Make sure we change back again when were done. 979 980 // For this test we need to give our current directory instead of the 981 // full path to the configuration as we already changed directories. 982 op, configCleanup, done := testOperationPlan(t, ".") 983 defer configCleanup() 984 defer done(t) 985 986 op.Workspace = testBackendSingleWorkspaceName 987 988 run, err := b.Operation(context.Background(), op) 989 if err != nil { 990 t.Fatalf("error starting operation: %v", err) 991 } 992 993 <-run.Done() 994 if run.Result != backend.OperationSuccess { 995 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 996 } 997 if run.PlanEmpty { 998 t.Fatalf("expected a non-empty plan") 999 } 1000 1001 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1002 if !strings.Contains(output, "Running plan in Terraform Cloud") { 1003 t.Fatalf("expected TFC header in output: %s", output) 1004 } 1005 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1006 t.Fatalf("expected plan summary in output: %s", output) 1007 } 1008 } 1009 1010 func TestCloud_planCostEstimation(t *testing.T) { 1011 b, bCleanup := testBackendWithName(t) 1012 defer bCleanup() 1013 1014 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cost-estimation") 1015 defer configCleanup() 1016 defer done(t) 1017 1018 op.Workspace = testBackendSingleWorkspaceName 1019 1020 run, err := b.Operation(context.Background(), op) 1021 if err != nil { 1022 t.Fatalf("error starting operation: %v", err) 1023 } 1024 1025 <-run.Done() 1026 if run.Result != backend.OperationSuccess { 1027 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1028 } 1029 if run.PlanEmpty { 1030 t.Fatalf("expected a non-empty plan") 1031 } 1032 1033 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1034 if !strings.Contains(output, "Running plan in Terraform Cloud") { 1035 t.Fatalf("expected TFC header in output: %s", output) 1036 } 1037 if !strings.Contains(output, "Resources: 1 of 1 estimated") { 1038 t.Fatalf("expected cost estimate result in output: %s", output) 1039 } 1040 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1041 t.Fatalf("expected plan summary in output: %s", output) 1042 } 1043 } 1044 1045 func TestCloud_planPolicyPass(t *testing.T) { 1046 b, bCleanup := testBackendWithName(t) 1047 defer bCleanup() 1048 1049 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-passed") 1050 defer configCleanup() 1051 defer done(t) 1052 1053 op.Workspace = testBackendSingleWorkspaceName 1054 1055 run, err := b.Operation(context.Background(), op) 1056 if err != nil { 1057 t.Fatalf("error starting operation: %v", err) 1058 } 1059 1060 <-run.Done() 1061 if run.Result != backend.OperationSuccess { 1062 t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) 1063 } 1064 if run.PlanEmpty { 1065 t.Fatalf("expected a non-empty plan") 1066 } 1067 1068 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1069 if !strings.Contains(output, "Running plan in Terraform Cloud") { 1070 t.Fatalf("expected TFC header in output: %s", output) 1071 } 1072 if !strings.Contains(output, "Sentinel Result: true") { 1073 t.Fatalf("expected policy check result in output: %s", output) 1074 } 1075 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1076 t.Fatalf("expected plan summary in output: %s", output) 1077 } 1078 } 1079 1080 func TestCloud_planPolicyHardFail(t *testing.T) { 1081 b, bCleanup := testBackendWithName(t) 1082 defer bCleanup() 1083 1084 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-hard-failed") 1085 defer configCleanup() 1086 1087 op.Workspace = testBackendSingleWorkspaceName 1088 1089 run, err := b.Operation(context.Background(), op) 1090 if err != nil { 1091 t.Fatalf("error starting operation: %v", err) 1092 } 1093 1094 <-run.Done() 1095 viewOutput := done(t) 1096 if run.Result == backend.OperationSuccess { 1097 t.Fatal("expected plan operation to fail") 1098 } 1099 if !run.PlanEmpty { 1100 t.Fatalf("expected plan to be empty") 1101 } 1102 1103 errOutput := viewOutput.Stderr() 1104 if !strings.Contains(errOutput, "hard failed") { 1105 t.Fatalf("expected a policy check error, got: %v", errOutput) 1106 } 1107 1108 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1109 if !strings.Contains(output, "Running plan in Terraform Cloud") { 1110 t.Fatalf("expected TFC header in output: %s", output) 1111 } 1112 if !strings.Contains(output, "Sentinel Result: false") { 1113 t.Fatalf("expected policy check result in output: %s", output) 1114 } 1115 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1116 t.Fatalf("expected plan summary in output: %s", output) 1117 } 1118 } 1119 1120 func TestCloud_planPolicySoftFail(t *testing.T) { 1121 b, bCleanup := testBackendWithName(t) 1122 defer bCleanup() 1123 1124 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-soft-failed") 1125 defer configCleanup() 1126 1127 op.Workspace = testBackendSingleWorkspaceName 1128 1129 run, err := b.Operation(context.Background(), op) 1130 if err != nil { 1131 t.Fatalf("error starting operation: %v", err) 1132 } 1133 1134 <-run.Done() 1135 viewOutput := done(t) 1136 if run.Result == backend.OperationSuccess { 1137 t.Fatal("expected plan operation to fail") 1138 } 1139 if !run.PlanEmpty { 1140 t.Fatalf("expected plan to be empty") 1141 } 1142 1143 errOutput := viewOutput.Stderr() 1144 if !strings.Contains(errOutput, "soft failed") { 1145 t.Fatalf("expected a policy check error, got: %v", errOutput) 1146 } 1147 1148 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1149 if !strings.Contains(output, "Running plan in Terraform Cloud") { 1150 t.Fatalf("expected TFC header in output: %s", output) 1151 } 1152 if !strings.Contains(output, "Sentinel Result: false") { 1153 t.Fatalf("expected policy check result in output: %s", output) 1154 } 1155 if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { 1156 t.Fatalf("expected plan summary in output: %s", output) 1157 } 1158 } 1159 1160 func TestCloud_planWithRemoteError(t *testing.T) { 1161 b, bCleanup := testBackendWithName(t) 1162 defer bCleanup() 1163 1164 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-error") 1165 defer configCleanup() 1166 defer done(t) 1167 1168 op.Workspace = testBackendSingleWorkspaceName 1169 1170 run, err := b.Operation(context.Background(), op) 1171 if err != nil { 1172 t.Fatalf("error starting operation: %v", err) 1173 } 1174 1175 <-run.Done() 1176 if run.Result == backend.OperationSuccess { 1177 t.Fatal("expected plan operation to fail") 1178 } 1179 if run.Result.ExitStatus() != 1 { 1180 t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) 1181 } 1182 1183 output := b.CLI.(*cli.MockUi).OutputWriter.String() 1184 if !strings.Contains(output, "Running plan in Terraform Cloud") { 1185 t.Fatalf("expected TFC header in output: %s", output) 1186 } 1187 if !strings.Contains(output, "null_resource.foo: 1 error") { 1188 t.Fatalf("expected plan error in output: %s", output) 1189 } 1190 } 1191 1192 func TestCloud_planJSONWithRemoteError(t *testing.T) { 1193 b, bCleanup := testBackendWithName(t) 1194 defer bCleanup() 1195 1196 stream, close := terminal.StreamsForTesting(t) 1197 1198 // Initialize the plan renderer 1199 b.renderer = &jsonformat.Renderer{ 1200 Streams: stream, 1201 Colorize: mockColorize(), 1202 } 1203 1204 op, configCleanup, done := testOperationPlan(t, "./testdata/plan-json-error") 1205 defer configCleanup() 1206 defer done(t) 1207 1208 op.Workspace = testBackendSingleWorkspaceName 1209 1210 mockSROWorkspace(t, b, op.Workspace) 1211 1212 run, err := b.Operation(context.Background(), op) 1213 if err != nil { 1214 t.Fatalf("error starting operation: %v", err) 1215 } 1216 1217 <-run.Done() 1218 if run.Result == backend.OperationSuccess { 1219 t.Fatal("expected plan operation to fail") 1220 } 1221 if run.Result.ExitStatus() != 1 { 1222 t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) 1223 } 1224 1225 outp := close(t) 1226 gotOut := outp.Stdout() 1227 1228 if !strings.Contains(gotOut, "Unsupported block type") { 1229 t.Fatalf("unexpected plan error in output: %s", gotOut) 1230 } 1231 } 1232 1233 func TestCloud_planOtherError(t *testing.T) { 1234 b, bCleanup := testBackendWithName(t) 1235 defer bCleanup() 1236 1237 op, configCleanup, done := testOperationPlan(t, "./testdata/plan") 1238 defer configCleanup() 1239 defer done(t) 1240 1241 op.Workspace = "network-error" // custom error response in backend_mock.go 1242 1243 _, err := b.Operation(context.Background(), op) 1244 if err == nil { 1245 t.Errorf("expected error, got success") 1246 } 1247 1248 if !strings.Contains(err.Error(), 1249 "Terraform Cloud returned an unexpected error:\n\nI'm a little teacup") { 1250 t.Fatalf("expected error message, got: %s", err.Error()) 1251 } 1252 }