mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
This extends statemgr.Persistent, statemgr.Locker and remote.Client to all expect context.Context parameters, and then updates all of the existing implementations of those interfaces to support them. All of the calls to statemgr.Persistent and statemgr.Locker methods outside of tests are consistently context.TODO() for now, because the caller landscape of these interfaces has some complications: 1. statemgr.Locker is also used by the clistate package for its state implementation that was derived from statemgr.Filesystem's predecessor, even though what clistate manages is not actually "state" in the sense of package statemgr. The callers of that are not yet ready to provide real contexts. In a future commit we'll either need to plumb context through to all of the clistate callers, or continue the effort to separate statemgr from clistate by introducing a clistate-specific "locker" API for it to use instead. 2. We call statemgr.Persistent and statemgr.Locker methods in situations where the active context might have already been cancelled, and so we'll need to make sure to ignore cancellation when calling those. This is mainly limited to PersistState and Unlock, since both need to be able to complete after a cancellation, but there are various codepaths that perform a Lock, Refresh, Persist, Unlock sequence and so it isn't yet clear where is the best place to enforce the invariant that Persist and Unlock must not be called with a cancelable context. We'll deal with that more in subsequent commits. Within the various state manager and remote client implementations the contexts _are_ wired together as best as possible with how these subsystems are already laid out, and so once we deal with the problems above and make callers provide suitable contexts they should be able to reach all of the leaf API clients that might want to generate OpenTelemetry traces. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
1975 lines
54 KiB
Go
1975 lines
54 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package cloud
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
tfe "github.com/hashicorp/go-tfe"
|
|
mocks "github.com/hashicorp/go-tfe/mocks"
|
|
version "github.com/hashicorp/go-version"
|
|
"github.com/mitchellh/cli"
|
|
gomock "go.uber.org/mock/gomock"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
"github.com/opentofu/opentofu/internal/cloud/cloudplan"
|
|
"github.com/opentofu/opentofu/internal/command/arguments"
|
|
"github.com/opentofu/opentofu/internal/command/clistate"
|
|
"github.com/opentofu/opentofu/internal/command/jsonformat"
|
|
"github.com/opentofu/opentofu/internal/command/views"
|
|
"github.com/opentofu/opentofu/internal/depsfile"
|
|
"github.com/opentofu/opentofu/internal/initwd"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/plans/planfile"
|
|
"github.com/opentofu/opentofu/internal/states/statemgr"
|
|
"github.com/opentofu/opentofu/internal/terminal"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
tfversion "github.com/opentofu/opentofu/version"
|
|
)
|
|
|
|
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(*testing.T) *terminal.TestOutput) {
|
|
t.Helper()
|
|
|
|
return testOperationApplyWithTimeout(t, configDir, 0)
|
|
}
|
|
|
|
func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(*testing.T) *terminal.TestOutput) {
|
|
t.Helper()
|
|
|
|
_, configLoader := initwd.MustLoadConfigForTests(t, configDir, "tests")
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
view := views.NewView(streams)
|
|
stateLockerView := views.NewStateLocker(arguments.ViewHuman, view)
|
|
operationView := views.NewOperation(arguments.ViewHuman, false, view)
|
|
|
|
// Many of our tests use an overridden "null" provider that's just in-memory
|
|
// inside the test process, not a separate plugin on disk.
|
|
depLocks := depsfile.NewLocks()
|
|
depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.opentofu.org/hashicorp/null"))
|
|
|
|
return &backend.Operation{
|
|
ConfigDir: configDir,
|
|
ConfigLoader: configLoader,
|
|
PlanRefresh: true,
|
|
StateLocker: clistate.NewLocker(timeout, stateLockerView),
|
|
Type: backend.OperationTypeApply,
|
|
View: operationView,
|
|
DependencyLocks: depLocks,
|
|
}, done
|
|
}
|
|
|
|
func TestCloud_applyBasic(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
}
|
|
|
|
stateMgr, _ := b.StateMgr(t.Context(), testBackendSingleWorkspaceName)
|
|
// An error suggests that the state was not unlocked after apply
|
|
if _, err := stateMgr.Lock(t.Context(), statemgr.NewLockInfo()); err != nil {
|
|
t.Fatalf("unexpected error locking state after apply: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyJSONBasic(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
stream, close := terminal.StreamsForTesting(t)
|
|
|
|
b.renderer = &jsonformat.Renderer{
|
|
Streams: stream,
|
|
Colorize: mockColorize(),
|
|
}
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-json")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
mockSROWorkspace(t, b, op.Workspace)
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
outp := close(t)
|
|
gotOut := outp.Stdout()
|
|
|
|
if !strings.Contains(gotOut, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summary in output: %s", gotOut)
|
|
}
|
|
if !strings.Contains(gotOut, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summary in output: %s", gotOut)
|
|
}
|
|
|
|
stateMgr, _ := b.StateMgr(t.Context(), testBackendSingleWorkspaceName)
|
|
// An error suggests that the state was not unlocked after apply
|
|
if _, err := stateMgr.Lock(t.Context(), statemgr.NewLockInfo()); err != nil {
|
|
t.Fatalf("unexpected error locking state after apply: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyJSONWithOutputs(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
stream, close := terminal.StreamsForTesting(t)
|
|
|
|
b.renderer = &jsonformat.Renderer{
|
|
Streams: stream,
|
|
Colorize: mockColorize(),
|
|
}
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-json-with-outputs")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
mockSROWorkspace(t, b, op.Workspace)
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
outp := close(t)
|
|
gotOut := outp.Stdout()
|
|
expectedSimpleOutput := `simple = [
|
|
"some",
|
|
"list",
|
|
]`
|
|
expectedSensitiveOutput := `secret = (sensitive value)`
|
|
expectedComplexOutput := `complex = {
|
|
keyA = {
|
|
someList = [
|
|
1,
|
|
2,
|
|
3,
|
|
]
|
|
}
|
|
keyB = {
|
|
someBool = true
|
|
someStr = "hello"
|
|
}
|
|
}`
|
|
|
|
if !strings.Contains(gotOut, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summary in output: %s", gotOut)
|
|
}
|
|
if !strings.Contains(gotOut, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summary in output: %s", gotOut)
|
|
}
|
|
if !strings.Contains(gotOut, "Outputs:") {
|
|
t.Fatalf("expected output header: %s", gotOut)
|
|
}
|
|
if !strings.Contains(gotOut, expectedSimpleOutput) {
|
|
t.Fatalf("expected output: %s, got: %s", expectedSimpleOutput, gotOut)
|
|
}
|
|
if !strings.Contains(gotOut, expectedSensitiveOutput) {
|
|
t.Fatalf("expected output: %s, got: %s", expectedSensitiveOutput, gotOut)
|
|
}
|
|
if !strings.Contains(gotOut, expectedComplexOutput) {
|
|
t.Fatalf("expected output: %s, got: %s", expectedComplexOutput, gotOut)
|
|
}
|
|
stateMgr, _ := b.StateMgr(t.Context(), testBackendSingleWorkspaceName)
|
|
// An error suggests that the state was not unlocked after apply
|
|
if _, err := stateMgr.Lock(t.Context(), statemgr.NewLockInfo()); err != nil {
|
|
t.Fatalf("unexpected error locking state after apply: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyCanceled(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
// Stop the run to simulate a Ctrl-C.
|
|
run.Stop()
|
|
|
|
<-run.Done()
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
|
|
stateMgr, _ := b.StateMgr(t.Context(), testBackendSingleWorkspaceName)
|
|
if _, err := stateMgr.Lock(t.Context(), statemgr.NewLockInfo()); err != nil {
|
|
t.Fatalf("unexpected error locking state after cancelling apply: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithoutPermissions(t *testing.T) {
|
|
b, bCleanup := testBackendWithTags(t)
|
|
defer bCleanup()
|
|
|
|
// Create a named workspace without permissions.
|
|
w, err := b.client.Workspaces.Create(
|
|
context.Background(),
|
|
b.organization,
|
|
tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("prod"),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
}
|
|
w.Permissions.CanQueueApply = false
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
op.UIOut = b.CLI
|
|
op.Workspace = "prod"
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
output := done(t)
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
|
|
errOutput := output.Stderr()
|
|
if !strings.Contains(errOutput, "Insufficient rights to apply changes") {
|
|
t.Fatalf("expected a permissions error, got: %v", errOutput)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithVCS(t *testing.T) {
|
|
b, bCleanup := testBackendWithTags(t)
|
|
defer bCleanup()
|
|
|
|
// Create a named workspace with a VCS.
|
|
_, err := b.client.Workspaces.Create(
|
|
context.Background(),
|
|
b.organization,
|
|
tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("prod"),
|
|
VCSRepo: &tfe.VCSRepoOptions{},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
}
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
op.Workspace = "prod"
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
output := done(t)
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if !run.PlanEmpty {
|
|
t.Fatalf("expected plan to be empty")
|
|
}
|
|
|
|
errOutput := output.Stderr()
|
|
if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") {
|
|
t.Fatalf("expected a VCS error, got: %v", errOutput)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithParallelism(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
if b.ContextOpts == nil {
|
|
b.ContextOpts = &tofu.ContextOpts{}
|
|
}
|
|
b.ContextOpts.Parallelism = 3
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
output := done(t)
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
|
|
errOutput := output.Stderr()
|
|
if !strings.Contains(errOutput, "parallelism values are currently not supported") {
|
|
t.Fatalf("expected a parallelism error, got: %v", errOutput)
|
|
}
|
|
}
|
|
|
|
// Apply with local plan file should fail.
|
|
func TestCloud_applyWithLocalPlan(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
op.PlanFile = planfile.NewWrappedLocal(&planfile.Reader{})
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
output := done(t)
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if !run.PlanEmpty {
|
|
t.Fatalf("expected plan to be empty")
|
|
}
|
|
|
|
errOutput := output.Stderr()
|
|
if !strings.Contains(errOutput, "saved local plan is not supported") {
|
|
t.Fatalf("expected a saved plan error, got: %v", errOutput)
|
|
}
|
|
}
|
|
|
|
// Apply with bookmark to an existing cloud plan that's in a confirmable state
|
|
// should work.
|
|
func TestCloud_applyWithCloudPlan(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-json")
|
|
defer done(t)
|
|
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
mockSROWorkspace(t, b, op.Workspace)
|
|
|
|
// Perform the plan before trying to apply it
|
|
ws, err := b.client.Workspaces.Read(context.Background(), b.organization, b.WorkspaceMapping.Name)
|
|
if err != nil {
|
|
t.Fatalf("Couldn't read workspace: %s", err)
|
|
}
|
|
|
|
planRun, err := b.plan(context.Background(), context.Background(), context.Background(), op, ws)
|
|
if err != nil {
|
|
t.Fatalf("Couldn't perform plan: %s", err)
|
|
}
|
|
|
|
// Synthesize a cloud plan file with the plan's run ID
|
|
pf := &cloudplan.SavedPlanBookmark{
|
|
RemotePlanFormat: 1,
|
|
RunID: planRun.ID,
|
|
Hostname: b.hostname,
|
|
}
|
|
op.PlanFile = planfile.NewWrappedCloud(pf)
|
|
|
|
// Start spying on the apply output (now that the plan's done)
|
|
stream, close := terminal.StreamsForTesting(t)
|
|
|
|
b.renderer = &jsonformat.Renderer{
|
|
Streams: stream,
|
|
Colorize: mockColorize(),
|
|
}
|
|
|
|
// Try apply
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
output := close(t)
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to succeed")
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected plan to not be empty")
|
|
}
|
|
|
|
gotOut := output.Stdout()
|
|
if !strings.Contains(gotOut, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summary in output: %s", gotOut)
|
|
}
|
|
|
|
stateMgr, _ := b.StateMgr(t.Context(), testBackendSingleWorkspaceName)
|
|
// An error suggests that the state was not unlocked after apply
|
|
if _, err := stateMgr.Lock(t.Context(), statemgr.NewLockInfo()); err != nil {
|
|
t.Fatalf("unexpected error locking state after apply: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithoutRefresh(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
op.PlanRefresh = false
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected plan to be non-empty")
|
|
}
|
|
|
|
// We should find a run inside the mock client that has refresh set
|
|
// to false.
|
|
runsAPI := b.client.Runs.(*MockRuns)
|
|
if got, want := len(runsAPI.Runs), 1; got != want {
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
}
|
|
for _, run := range runsAPI.Runs {
|
|
if diff := cmp.Diff(false, run.Refresh); diff != "" {
|
|
t.Errorf("wrong Refresh setting in the created run\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithRefreshOnly(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
op.PlanMode = plans.RefreshOnlyMode
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected plan to be non-empty")
|
|
}
|
|
|
|
// We should find a run inside the mock client that has refresh-only set
|
|
// to true.
|
|
runsAPI := b.client.Runs.(*MockRuns)
|
|
if got, want := len(runsAPI.Runs), 1; got != want {
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
}
|
|
for _, run := range runsAPI.Runs {
|
|
if diff := cmp.Diff(true, run.RefreshOnly); diff != "" {
|
|
t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithTarget(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
|
|
|
|
op.Targets = []addrs.Targetable{addr}
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to succeed")
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected plan to be non-empty")
|
|
}
|
|
|
|
// We should find a run inside the mock client that has the same
|
|
// target address we requested above.
|
|
runsAPI := b.client.Runs.(*MockRuns)
|
|
if got, want := len(runsAPI.Runs), 1; got != want {
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
}
|
|
for _, run := range runsAPI.Runs {
|
|
if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" {
|
|
t.Errorf("wrong TargetAddrs in the created run\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Applying with an exclude flag should error
|
|
func TestCloud_applyWithExclude(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
|
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
op.Excludes = []addrs.Targetable{addr}
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
output := done(t)
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if !run.PlanEmpty {
|
|
t.Fatalf("expected plan to be empty")
|
|
}
|
|
|
|
errOutput := output.Stderr()
|
|
if !strings.Contains(errOutput, "-exclude option is not supported") {
|
|
t.Fatalf("expected -exclude option is not supported error, got: %v", errOutput)
|
|
}
|
|
|
|
stateMgr, _ := b.StateMgr(t.Context(), testBackendSingleWorkspaceName)
|
|
// An error suggests that the state was not unlocked after apply
|
|
if _, err := stateMgr.Lock(t.Context(), statemgr.NewLockInfo()); err != nil {
|
|
t.Fatalf("unexpected error locking state after failed apply: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithReplace(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo")
|
|
|
|
op.ForceReplace = []addrs.AbsResourceInstance{addr}
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatal("expected plan operation to succeed")
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected plan to be non-empty")
|
|
}
|
|
|
|
// We should find a run inside the mock client that has the same
|
|
// refresh address we requested above.
|
|
runsAPI := b.client.Runs.(*MockRuns)
|
|
if got, want := len(runsAPI.Runs), 1; got != want {
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
}
|
|
for _, run := range runsAPI.Runs {
|
|
if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" {
|
|
t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithRequiredVariables(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-variables")
|
|
defer done(t)
|
|
|
|
op.Variables = testVariables(tofu.ValueFromNamedFile, "foo") // "bar" variable value missing
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
// The usual error of a required variable being missing is deferred and the operation
|
|
// is successful
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatal("expected plan operation to succeed")
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("unexpected TFC header in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyNoConfig(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/empty")
|
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
output := done(t)
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if !run.PlanEmpty {
|
|
t.Fatalf("expected plan to be empty")
|
|
}
|
|
|
|
errOutput := output.Stderr()
|
|
if !strings.Contains(errOutput, "configuration files found") {
|
|
t.Fatalf("expected configuration files error, got: %v", errOutput)
|
|
}
|
|
|
|
stateMgr, _ := b.StateMgr(t.Context(), testBackendSingleWorkspaceName)
|
|
// An error suggests that the state was not unlocked after apply
|
|
if _, err := stateMgr.Lock(t.Context(), statemgr.NewLockInfo()); err != nil {
|
|
t.Fatalf("unexpected error locking state after failed apply: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyNoChanges(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-no-changes")
|
|
defer done(t)
|
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if !run.PlanEmpty {
|
|
t.Fatalf("expected plan to be empty")
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") {
|
|
t.Fatalf("expected no changes in plan summery: %s", output)
|
|
}
|
|
if !strings.Contains(output, "Sentinel Result: true") {
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyNoApprove(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "no",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
output := done(t)
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if !run.PlanEmpty {
|
|
t.Fatalf("expected plan to be empty")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
errOutput := output.Stderr()
|
|
if !strings.Contains(errOutput, "Apply discarded") {
|
|
t.Fatalf("expected an apply discarded error, got: %v", errOutput)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyAutoApprove(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
ctrl := gomock.NewController(t)
|
|
|
|
applyMock := mocks.NewMockApplies(ctrl)
|
|
// This needs three new lines because we check for a minimum of three lines
|
|
// in the parsing of logs in `opApply` function.
|
|
logs := strings.NewReader(applySuccessOneResourceAdded)
|
|
applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
|
|
b.client.Applies = applyMock
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "no",
|
|
})
|
|
|
|
op.AutoApprove = true
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) != 1 {
|
|
t.Fatalf("expected an unused answer, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyApprovedExternally(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "wait-for-external-update",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
ctx := context.Background()
|
|
|
|
run, err := b.Operation(ctx, op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
// Wait 50 milliseconds to make sure the run started.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
wl, err := b.client.Workspaces.List(
|
|
ctx,
|
|
b.organization,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error listing workspaces: %v", err)
|
|
}
|
|
if len(wl.Items) != 1 {
|
|
t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items))
|
|
}
|
|
|
|
rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error listing runs: %v", err)
|
|
}
|
|
if len(rl.Items) != 1 {
|
|
t.Fatalf("expected 1 run, got %d runs", len(rl.Items))
|
|
}
|
|
|
|
err = b.client.Runs.Apply(context.Background(), rl.Items[0].ID, tfe.RunApplyOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error approving run: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "approved using the UI or API") {
|
|
t.Fatalf("expected external approval in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyDiscardedExternally(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "wait-for-external-update",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
ctx := context.Background()
|
|
|
|
run, err := b.Operation(ctx, op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
// Wait 50 milliseconds to make sure the run started.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
wl, err := b.client.Workspaces.List(
|
|
ctx,
|
|
b.organization,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error listing workspaces: %v", err)
|
|
}
|
|
if len(wl.Items) != 1 {
|
|
t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items))
|
|
}
|
|
|
|
rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error listing runs: %v", err)
|
|
}
|
|
if len(rl.Items) != 1 {
|
|
t.Fatalf("expected 1 run, got %d runs", len(rl.Items))
|
|
}
|
|
|
|
err = b.client.Runs.Discard(context.Background(), rl.Items[0].ID, tfe.RunDiscardOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error discarding run: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if !run.PlanEmpty {
|
|
t.Fatalf("expected plan to be empty")
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "discarded using the UI or API") {
|
|
t.Fatalf("expected external discard output: %s", output)
|
|
}
|
|
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("unexpected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithAutoApprove(t *testing.T) {
|
|
b, bCleanup := testBackendWithTags(t)
|
|
defer bCleanup()
|
|
ctrl := gomock.NewController(t)
|
|
|
|
applyMock := mocks.NewMockApplies(ctrl)
|
|
// This needs three new lines because we check for a minimum of three lines
|
|
// in the parsing of logs in `opApply` function.
|
|
logs := strings.NewReader(applySuccessOneResourceAdded)
|
|
applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
|
|
b.client.Applies = applyMock
|
|
|
|
// Create a named workspace that auto applies.
|
|
_, err := b.client.Workspaces.Create(
|
|
context.Background(),
|
|
b.organization,
|
|
tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("prod"),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
}
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = "prod"
|
|
op.AutoApprove = true
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) != 1 {
|
|
t.Fatalf("expected an unused answer, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyForceLocal(t *testing.T) {
|
|
// Set TF_FORCE_LOCAL_BACKEND so the cloud backend will use
|
|
// the local backend with itself as embedded backend.
|
|
t.Setenv("TF_FORCE_LOCAL_BACKEND", "1")
|
|
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
|
|
op.View = view
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("unexpected TFC header in output: %s", output)
|
|
}
|
|
if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summary in output: %s", output)
|
|
}
|
|
if !run.State.HasManagedResourceInstanceObjects() {
|
|
t.Fatalf("expected resources in state")
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWorkspaceWithoutOperations(t *testing.T) {
|
|
b, bCleanup := testBackendWithTags(t)
|
|
defer bCleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create a named workspace that doesn't allow operations.
|
|
_, err := b.client.Workspaces.Create(
|
|
ctx,
|
|
b.organization,
|
|
tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("no-operations"),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
}
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = "no-operations"
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
|
|
op.View = view
|
|
|
|
run, err := b.Operation(ctx, op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("unexpected TFC header in output: %s", output)
|
|
}
|
|
if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summary in output: %s", output)
|
|
}
|
|
if !run.State.HasManagedResourceInstanceObjects() {
|
|
t.Fatalf("expected resources in state")
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyLockTimeout(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Retrieve the workspace used to run this operation in.
|
|
w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name)
|
|
if err != nil {
|
|
t.Fatalf("error retrieving workspace: %v", err)
|
|
}
|
|
|
|
// Create a new configuration version.
|
|
c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{})
|
|
if err != nil {
|
|
t.Fatalf("error creating configuration version: %v", err)
|
|
}
|
|
|
|
// Create a pending run to block this run.
|
|
_, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{
|
|
ConfigurationVersion: c,
|
|
Workspace: w,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error creating pending run: %v", err)
|
|
}
|
|
|
|
op, done := testOperationApplyWithTimeout(t, "./testdata/apply", 50*time.Millisecond)
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"cancel": "yes",
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
_, err = b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
sigint := make(chan os.Signal, 1)
|
|
signal.Notify(sigint, syscall.SIGINT)
|
|
select {
|
|
case <-sigint:
|
|
// Stop redirecting SIGINT signals.
|
|
signal.Stop(sigint)
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatalf("expected lock timeout after 50 milliseconds, waited 200 milliseconds")
|
|
}
|
|
|
|
if len(input.answers) != 2 {
|
|
t.Fatalf("expected unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "Lock timeout exceeded") {
|
|
t.Fatalf("expected lock timeout error in output: %s", output)
|
|
}
|
|
if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("unexpected plan summery in output: %s", output)
|
|
}
|
|
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("unexpected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyDestroy(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-destroy")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.PlanMode = plans.DestroyMode
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") {
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyDestroyNoConfig(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op, done := testOperationApply(t, "./testdata/empty")
|
|
defer done(t)
|
|
|
|
op.PlanMode = plans.DestroyMode
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyJSONWithProvisioner(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
stream, close := terminal.StreamsForTesting(t)
|
|
|
|
b.renderer = &jsonformat.Renderer{
|
|
Streams: stream,
|
|
Colorize: mockColorize(),
|
|
}
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-json-with-provisioner")
|
|
defer done(t)
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
mockSROWorkspace(t, b, op.Workspace)
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
outp := close(t)
|
|
gotOut := outp.Stdout()
|
|
if !strings.Contains(gotOut, "null_resource.foo: Provisioning with 'local-exec'") {
|
|
t.Fatalf("expected provisioner local-exec start in logs: %s", gotOut)
|
|
}
|
|
|
|
if !strings.Contains(gotOut, "null_resource.foo: (local-exec):") {
|
|
t.Fatalf("expected provisioner local-exec progress in logs: %s", gotOut)
|
|
}
|
|
|
|
if !strings.Contains(gotOut, "Hello World!") {
|
|
t.Fatalf("expected provisioner local-exec output in logs: %s", gotOut)
|
|
}
|
|
|
|
stateMgr, _ := b.StateMgr(t.Context(), testBackendSingleWorkspaceName)
|
|
// An error suggests that the state was not unlocked after apply
|
|
if _, err := stateMgr.Lock(t.Context(), statemgr.NewLockInfo()); err != nil {
|
|
t.Fatalf("unexpected error locking state after apply: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyJSONWithProvisionerError(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
stream, close := terminal.StreamsForTesting(t)
|
|
|
|
b.renderer = &jsonformat.Renderer{
|
|
Streams: stream,
|
|
Colorize: mockColorize(),
|
|
}
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-json-with-provisioner-error")
|
|
defer done(t)
|
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
mockSROWorkspace(t, b, op.Workspace)
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
|
|
outp := close(t)
|
|
gotOut := outp.Stdout()
|
|
|
|
if !strings.Contains(gotOut, "local-exec provisioner error") {
|
|
t.Fatalf("unexpected error in apply logs: %s", gotOut)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyPolicyPass(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-policy-passed")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "Sentinel Result: true") {
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyPolicyHardFail(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-policy-hard-failed")
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
viewOutput := done(t)
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if !run.PlanEmpty {
|
|
t.Fatalf("expected plan to be empty")
|
|
}
|
|
|
|
if len(input.answers) != 1 {
|
|
t.Fatalf("expected an unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
errOutput := viewOutput.Stderr()
|
|
if !strings.Contains(errOutput, "hard failed") {
|
|
t.Fatalf("expected a policy check error, got: %v", errOutput)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "Sentinel Result: false") {
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
|
}
|
|
if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("unexpected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyPolicySoftFail(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"override": "override",
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.AutoApprove = false
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) > 0 {
|
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "Sentinel Result: false") {
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
ctrl := gomock.NewController(t)
|
|
|
|
policyCheckMock := mocks.NewMockPolicyChecks(ctrl)
|
|
// This needs three new lines because we check for a minimum of three lines
|
|
// in the parsing of logs in `opApply` function.
|
|
logs := strings.NewReader(fmt.Sprintf("%s\n%s", sentinelSoftFail, applySuccessOneResourceAdded))
|
|
|
|
pc := &tfe.PolicyCheck{
|
|
ID: "pc-1",
|
|
Actions: &tfe.PolicyActions{
|
|
IsOverridable: true,
|
|
},
|
|
Permissions: &tfe.PolicyPermissions{
|
|
CanOverride: true,
|
|
},
|
|
Scope: tfe.PolicyScopeOrganization,
|
|
Status: tfe.PolicySoftFailed,
|
|
}
|
|
policyCheckMock.EXPECT().Read(gomock.Any(), gomock.Any()).Return(pc, nil)
|
|
policyCheckMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
|
|
policyCheckMock.EXPECT().Override(gomock.Any(), gomock.Any()).Return(nil, nil)
|
|
b.client.PolicyChecks = policyCheckMock
|
|
applyMock := mocks.NewMockApplies(ctrl)
|
|
// This needs three new lines because we check for a minimum of three lines
|
|
// in the parsing of logs in `opApply` function.
|
|
logs = strings.NewReader("\n\n\n1 added, 0 changed, 0 destroyed")
|
|
applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
|
|
b.client.Applies = applyMock
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
|
|
|
input := testInput(t, map[string]string{})
|
|
|
|
op.AutoApprove = true
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
viewOutput := done(t)
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to success due to auto-approve")
|
|
}
|
|
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected plan to not be empty, plan operation completed without error")
|
|
}
|
|
|
|
if len(input.answers) != 0 {
|
|
t.Fatalf("expected no answers, got: %v", input.answers)
|
|
}
|
|
|
|
errOutput := viewOutput.Stderr()
|
|
if strings.Contains(errOutput, "soft failed") {
|
|
t.Fatalf("expected no policy check errors, instead got: %v", errOutput)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Sentinel Result: false") {
|
|
t.Fatalf("expected policy check to be false, instead got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "Apply complete!") {
|
|
t.Fatalf("expected apply to be complete, instead got: %s", output)
|
|
}
|
|
|
|
if !strings.Contains(output, "Resources: 1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected resources, instead got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyPolicySoftFailAutoApprove(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
ctrl := gomock.NewController(t)
|
|
|
|
applyMock := mocks.NewMockApplies(ctrl)
|
|
// This needs three new lines because we check for a minimum of three lines
|
|
// in the parsing of logs in `opApply` function.
|
|
logs := strings.NewReader(applySuccessOneResourceAdded)
|
|
applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
|
|
b.client.Applies = applyMock
|
|
|
|
// Create a named workspace that auto applies.
|
|
_, err := b.client.Workspaces.Create(
|
|
context.Background(),
|
|
b.organization,
|
|
tfe.WorkspaceCreateOptions{
|
|
Name: tfe.String("prod"),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
}
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
|
defer done(t)
|
|
|
|
input := testInput(t, map[string]string{
|
|
"override": "override",
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = "prod"
|
|
op.AutoApprove = true
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
if run.PlanEmpty {
|
|
t.Fatalf("expected a non-empty plan")
|
|
}
|
|
|
|
if len(input.answers) != 2 {
|
|
t.Fatalf("expected an unused answer, got: %v", input.answers)
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "Running apply in cloud backend") {
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
|
t.Fatalf("expected plan summery in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "Sentinel Result: false") {
|
|
t.Fatalf("expected policy check result in output: %s", output)
|
|
}
|
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
|
t.Fatalf("expected apply summery in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyWithRemoteError(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-with-error")
|
|
defer done(t)
|
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if run.Result.ExitStatus() != 1 {
|
|
t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus())
|
|
}
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
if !strings.Contains(output, "null_resource.foo: 1 error") {
|
|
t.Fatalf("expected apply error in output: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyJSONWithRemoteError(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
stream, close := terminal.StreamsForTesting(t)
|
|
|
|
b.renderer = &jsonformat.Renderer{
|
|
Streams: stream,
|
|
Colorize: mockColorize(),
|
|
}
|
|
|
|
op, done := testOperationApply(t, "./testdata/apply-json-with-error")
|
|
defer done(t)
|
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
mockSROWorkspace(t, b, op.Workspace)
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
<-run.Done()
|
|
if run.Result == backend.OperationSuccess {
|
|
t.Fatal("expected apply operation to fail")
|
|
}
|
|
if run.Result.ExitStatus() != 1 {
|
|
t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus())
|
|
}
|
|
|
|
outp := close(t)
|
|
gotOut := outp.Stdout()
|
|
|
|
if !strings.Contains(gotOut, "Unsupported block type") {
|
|
t.Fatalf("unexpected plan error in output: %s", gotOut)
|
|
}
|
|
}
|
|
|
|
func TestCloud_applyVersionCheck(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
localVersion string
|
|
remoteVersion string
|
|
forceLocal bool
|
|
executionMode string
|
|
wantErr string
|
|
}{
|
|
"versions can be different for remote apply": {
|
|
localVersion: "0.14.0",
|
|
remoteVersion: "0.13.5",
|
|
executionMode: "remote",
|
|
},
|
|
"versions can be different for local apply": {
|
|
localVersion: "0.14.0",
|
|
remoteVersion: "0.13.5",
|
|
executionMode: "local",
|
|
},
|
|
"force local with remote operations and different versions is acceptable": {
|
|
localVersion: "0.14.0",
|
|
remoteVersion: "0.14.0-acme-provider-bundle",
|
|
forceLocal: true,
|
|
executionMode: "remote",
|
|
},
|
|
"no error if versions are identical": {
|
|
localVersion: "0.14.0",
|
|
remoteVersion: "0.14.0",
|
|
forceLocal: true,
|
|
executionMode: "remote",
|
|
},
|
|
"no error if force local but workspace has remote operations disabled": {
|
|
localVersion: "0.14.0",
|
|
remoteVersion: "0.13.5",
|
|
forceLocal: true,
|
|
executionMode: "local",
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
b, bCleanup := testBackendWithName(t)
|
|
defer bCleanup()
|
|
|
|
// SETUP: Save original local version state and restore afterwards
|
|
p := tfversion.Prerelease
|
|
v := tfversion.Version
|
|
s := tfversion.SemVer
|
|
defer func() {
|
|
tfversion.Prerelease = p
|
|
tfversion.Version = v
|
|
tfversion.SemVer = s
|
|
}()
|
|
|
|
// SETUP: Set local version for the test case
|
|
tfversion.Prerelease = ""
|
|
tfversion.Version = tc.localVersion
|
|
tfversion.SemVer = version.Must(version.NewSemver(tc.localVersion))
|
|
|
|
// SETUP: Set force local for the test case
|
|
b.forceLocal = tc.forceLocal
|
|
|
|
ctx := context.Background()
|
|
|
|
// SETUP: set the operations and Terraform Version fields on the
|
|
// remote workspace
|
|
_, err := b.client.Workspaces.Update(
|
|
ctx,
|
|
b.organization,
|
|
b.WorkspaceMapping.Name,
|
|
tfe.WorkspaceUpdateOptions{
|
|
ExecutionMode: tfe.String(tc.executionMode),
|
|
TerraformVersion: tfe.String(tc.remoteVersion),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
}
|
|
|
|
// RUN: prepare the apply operation and run it
|
|
op, opDone := testOperationApply(t, "./testdata/apply")
|
|
defer opDone(t)
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
|
|
op.View = view
|
|
|
|
input := testInput(t, map[string]string{
|
|
"approve": "yes",
|
|
})
|
|
|
|
op.UIIn = input
|
|
op.UIOut = b.CLI
|
|
op.Workspace = testBackendSingleWorkspaceName
|
|
|
|
run, err := b.Operation(ctx, op)
|
|
if err != nil {
|
|
t.Fatalf("error starting operation: %v", err)
|
|
}
|
|
|
|
// RUN: wait for completion
|
|
<-run.Done()
|
|
output := done(t)
|
|
|
|
if tc.wantErr != "" {
|
|
// ASSERT: if the test case wants an error, check for failure
|
|
// and the error message
|
|
if run.Result != backend.OperationFailure {
|
|
t.Fatalf("expected run to fail, but result was %#v", run.Result)
|
|
}
|
|
errOutput := output.Stderr()
|
|
if !strings.Contains(errOutput, tc.wantErr) {
|
|
t.Fatalf("missing error %q\noutput: %s", tc.wantErr, errOutput)
|
|
}
|
|
} else {
|
|
// ASSERT: otherwise, check for success and appropriate output
|
|
// based on whether the run should be local or remote
|
|
if run.Result != backend.OperationSuccess {
|
|
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
|
|
}
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
hasRemote := strings.Contains(output, "Running apply in cloud backend")
|
|
hasSummary := strings.Contains(output, "1 added, 0 changed, 0 destroyed")
|
|
hasResources := run.State.HasManagedResourceInstanceObjects()
|
|
if !tc.forceLocal && !isLocalExecutionMode(tc.executionMode) {
|
|
if !hasRemote {
|
|
t.Errorf("missing TFC header in output: %s", output)
|
|
}
|
|
if !hasSummary {
|
|
t.Errorf("expected apply summary in output: %s", output)
|
|
}
|
|
} else {
|
|
if hasRemote {
|
|
t.Errorf("unexpected TFC header in output: %s", output)
|
|
}
|
|
if !hasResources {
|
|
t.Errorf("expected resources in state")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const applySuccessOneResourceAdded = `
|
|
OpenTofu v0.11.10
|
|
|
|
Initializing plugins and modules...
|
|
null_resource.hello: Creating...
|
|
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
|
|
|
|
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
|
`
|
|
|
|
const sentinelSoftFail = `
|
|
Sentinel Result: false
|
|
|
|
Sentinel evaluated to false because one or more Sentinel policies evaluated
|
|
to false. This false was not due to an undefined value or runtime error.
|
|
|
|
1 policies evaluated.
|
|
|
|
## Policy 1: Passthrough.sentinel (soft-mandatory)
|
|
|
|
Result: false
|
|
|
|
FALSE - Passthrough.sentinel:1:1 - Rule "main"
|
|
`
|