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