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