2021-08-12 21:30:24 +02:00
|
|
|
package cloud
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-09-22 23:53:33 +02:00
|
|
|
"fmt"
|
2021-08-12 21:30:24 +02:00
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2021-09-22 23:53:33 +02:00
|
|
|
gomock "github.com/golang/mock/gomock"
|
2021-08-12 21:30:24 +02:00
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
tfe "github.com/hashicorp/go-tfe"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
|
|
"github.com/hashicorp/terraform/internal/backend"
|
|
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
|
|
|
"github.com/hashicorp/terraform/internal/command/clistate"
|
|
|
|
"github.com/hashicorp/terraform/internal/command/views"
|
2021-10-08 01:56:25 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/depsfile"
|
2021-08-12 21:30:24 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/initwd"
|
|
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
|
|
"github.com/hashicorp/terraform/internal/plans/planfile"
|
|
|
|
"github.com/hashicorp/terraform/internal/states/statemgr"
|
|
|
|
"github.com/hashicorp/terraform/internal/terminal"
|
|
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
|
|
|
tfversion "github.com/hashicorp/terraform/version"
|
|
|
|
"github.com/mitchellh/cli"
|
|
|
|
)
|
|
|
|
|
|
|
|
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), 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(), func(*testing.T) *terminal.TestOutput) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
|
|
|
|
|
|
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
|
|
view := views.NewView(streams)
|
|
|
|
stateLockerView := views.NewStateLocker(arguments.ViewHuman, view)
|
|
|
|
operationView := views.NewOperation(arguments.ViewHuman, false, view)
|
|
|
|
|
2021-10-08 01:56:25 +02:00
|
|
|
// 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.terraform.io/hashicorp/null"))
|
|
|
|
|
2021-08-12 21:30:24 +02:00
|
|
|
return &backend.Operation{
|
2021-10-08 01:56:25 +02:00
|
|
|
ConfigDir: configDir,
|
|
|
|
ConfigLoader: configLoader,
|
|
|
|
PlanRefresh: true,
|
|
|
|
StateLocker: clistate.NewLocker(timeout, stateLockerView),
|
|
|
|
Type: backend.OperationTypeApply,
|
|
|
|
View: operationView,
|
|
|
|
DependencyLocks: depLocks,
|
2021-08-12 21:30:24 +02:00
|
|
|
}, configCleanup, done
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyBasic(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-09-20 20:07:53 +02:00
|
|
|
stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
|
2021-08-12 21:30:24 +02:00
|
|
|
// An error suggests that the state was not unlocked after apply
|
|
|
|
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
|
|
|
|
t.Fatalf("unexpected error locking state after apply: %s", err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyCanceled(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2021-09-20 20:07:53 +02:00
|
|
|
stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
|
2021-08-12 21:30:24 +02:00
|
|
|
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
|
|
|
|
t.Fatalf("unexpected error locking state after cancelling apply: %s", err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyWithoutPermissions(t *testing.T) {
|
2021-10-11 23:26:07 +02:00
|
|
|
b, bCleanup := testBackendWithTags(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
// Create a named workspace without permissions.
|
|
|
|
w, err := b.client.Workspaces.Create(
|
|
|
|
context.Background(),
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
2021-10-11 23:26:07 +02:00
|
|
|
Name: tfe.String("prod"),
|
2021-08-12 21:30:24 +02:00
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
w.Permissions.CanQueueApply = false
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
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) {
|
2021-10-11 23:26:07 +02:00
|
|
|
b, bCleanup := testBackendWithTags(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
// Create a named workspace with a VCS.
|
|
|
|
_, err := b.client.Workspaces.Create(
|
|
|
|
context.Background(),
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
2021-10-11 23:26:07 +02:00
|
|
|
Name: tfe.String("prod"),
|
2021-08-12 21:30:24 +02:00
|
|
|
VCSRepo: &tfe.VCSRepoOptions{},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
|
2021-09-22 23:59:47 +02:00
|
|
|
if b.ContextOpts == nil {
|
|
|
|
b.ContextOpts = &terraform.ContextOpts{}
|
|
|
|
}
|
|
|
|
b.ContextOpts.Parallelism = 3
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyWithPlan(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
op.PlanFile = &planfile.Reader{}
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 plan is currently not supported") {
|
|
|
|
t.Fatalf("expected a saved plan error, got: %v", errOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyWithoutRefresh(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
op.PlanRefresh = false
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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.
|
2021-09-07 22:35:32 +02:00
|
|
|
runsAPI := b.client.Runs.(*MockRuns)
|
|
|
|
if got, want := len(runsAPI.Runs), 1; got != want {
|
2021-08-12 21:30:24 +02:00
|
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
|
|
}
|
2021-09-07 22:35:32 +02:00
|
|
|
for _, run := range runsAPI.Runs {
|
2021-08-12 21:30:24 +02:00
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
op.PlanMode = plans.RefreshOnlyMode
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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.
|
2021-09-07 22:35:32 +02:00
|
|
|
runsAPI := b.client.Runs.(*MockRuns)
|
|
|
|
if got, want := len(runsAPI.Runs), 1; got != want {
|
2021-08-12 21:30:24 +02:00
|
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
|
|
}
|
2021-09-07 22:35:32 +02:00
|
|
|
for _, run := range runsAPI.Runs {
|
2021-08-12 21:30:24 +02:00
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
|
|
|
|
|
|
|
|
op.Targets = []addrs.Targetable{addr}
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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.
|
2021-09-07 22:35:32 +02:00
|
|
|
runsAPI := b.client.Runs.(*MockRuns)
|
|
|
|
if got, want := len(runsAPI.Runs), 1; got != want {
|
2021-08-12 21:30:24 +02:00
|
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
|
|
}
|
2021-09-07 22:35:32 +02:00
|
|
|
for _, run := range runsAPI.Runs {
|
2021-08-12 21:30:24 +02:00
|
|
|
if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" {
|
|
|
|
t.Errorf("wrong TargetAddrs in the created run\n%s", diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyWithReplace(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo")
|
|
|
|
|
|
|
|
op.ForceReplace = []addrs.AbsResourceInstance{addr}
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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.
|
2021-09-07 22:35:32 +02:00
|
|
|
runsAPI := b.client.Runs.(*MockRuns)
|
|
|
|
if got, want := len(runsAPI.Runs), 1; got != want {
|
2021-08-12 21:30:24 +02:00
|
|
|
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
|
|
|
|
}
|
2021-09-07 22:35:32 +02:00
|
|
|
for _, run := range runsAPI.Runs {
|
2021-08-12 21:30:24 +02:00
|
|
|
if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" {
|
|
|
|
t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-23 19:35:54 +02:00
|
|
|
func TestCloud_applyWithRequiredVariables(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-variables")
|
|
|
|
defer configCleanup()
|
2021-08-23 19:35:54 +02:00
|
|
|
defer done(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
|
2021-08-23 19:35:54 +02:00
|
|
|
op.Variables = testVariables(terraform.ValueFromNamedFile, "foo") // "bar" variable value missing
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
run, err := b.Operation(context.Background(), op)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error starting operation: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-run.Done()
|
2021-08-23 19:35:54 +02:00
|
|
|
// 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")
|
2021-08-12 21:30:24 +02:00
|
|
|
}
|
|
|
|
|
2021-08-23 19:35:54 +02:00
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if !strings.Contains(output, "Running apply in Terraform Cloud") {
|
|
|
|
t.Fatalf("unexpected TFC header in output: %s", output)
|
2021-08-12 21:30:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyNoConfig(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/empty")
|
|
|
|
defer configCleanup()
|
|
|
|
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-09-20 20:07:53 +02:00
|
|
|
stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
|
2021-08-12 21:30:24 +02:00
|
|
|
// An error suggests that the state was not unlocked after apply
|
|
|
|
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
|
|
|
|
t.Fatalf("unexpected error locking state after failed apply: %s", err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyNoChanges(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-no-changes")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "no",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
2021-09-22 23:53:33 +02:00
|
|
|
ctrl := gomock.NewController(t)
|
|
|
|
|
|
|
|
applyMock := tfe.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
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "no",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.AutoApprove = true
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "wait-for-external-update",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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,
|
2021-09-15 07:04:15 +02:00
|
|
|
tfe.WorkspaceListOptions{},
|
2021-08-12 21:30:24 +02:00
|
|
|
)
|
|
|
|
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, tfe.RunListOptions{})
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "wait-for-external-update",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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,
|
2021-09-15 07:04:15 +02:00
|
|
|
tfe.WorkspaceListOptions{},
|
2021-08-12 21:30:24 +02:00
|
|
|
)
|
|
|
|
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, tfe.RunListOptions{})
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-22 23:53:33 +02:00
|
|
|
func TestCloud_applyWithAutoApprove(t *testing.T) {
|
2021-10-11 23:26:07 +02:00
|
|
|
b, bCleanup := testBackendWithTags(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
2021-09-22 23:53:33 +02:00
|
|
|
ctrl := gomock.NewController(t)
|
|
|
|
|
|
|
|
applyMock := tfe.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
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
// Create a named workspace that auto applies.
|
|
|
|
_, err := b.client.Workspaces.Create(
|
|
|
|
context.Background(),
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
2021-10-11 23:26:07 +02:00
|
|
|
Name: tfe.String("prod"),
|
2021-08-12 21:30:24 +02:00
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = "prod"
|
2021-09-22 23:53:33 +02:00
|
|
|
op.AutoApprove = true
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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.
|
|
|
|
if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
|
|
|
|
t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
|
|
|
|
}
|
|
|
|
defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
|
|
|
|
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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)
|
|
|
|
}
|
2021-10-21 15:55:27 +02:00
|
|
|
if !run.State.HasManagedResourceInstanceObjects() {
|
2021-08-12 21:30:24 +02:00
|
|
|
t.Fatalf("expected resources in state")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyWorkspaceWithoutOperations(t *testing.T) {
|
2021-10-11 23:26:07 +02:00
|
|
|
b, bCleanup := testBackendWithTags(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// Create a named workspace that doesn't allow operations.
|
|
|
|
_, err := b.client.Workspaces.Create(
|
|
|
|
ctx,
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
2021-10-11 23:26:07 +02:00
|
|
|
Name: tfe.String("no-operations"),
|
2021-08-12 21:30:24 +02:00
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
|
|
|
|
defer configCleanup()
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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)
|
|
|
|
}
|
2021-10-21 15:55:27 +02:00
|
|
|
if !run.State.HasManagedResourceInstanceObjects() {
|
2021-08-12 21:30:24 +02:00
|
|
|
t.Fatalf("expected resources in state")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCloud_applyLockTimeout(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// Retrieve the workspace used to run this operation in.
|
2021-09-20 20:07:53 +02:00
|
|
|
w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name)
|
2021-08-12 21:30:24 +02:00
|
|
|
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, configCleanup, done := testOperationApplyWithTimeout(t, "./testdata/apply", 50*time.Millisecond)
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"cancel": "yes",
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
_, 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 Terraform Cloud") {
|
|
|
|
t.Fatalf("expected TFC header in output: %s", output)
|
|
|
|
}
|
|
|
|
if !strings.Contains(output, "Lock timeout exceeded") {
|
|
|
|
t.Fatalf("expected lock timout 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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-destroy")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.PlanMode = plans.DestroyMode
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/empty")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
op.PlanMode = plans.DestroyMode
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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_applyPolicyPass(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-passed")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-hard-failed")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"override": "override",
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.AutoApprove = false
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud") {
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
2021-09-22 23:53:33 +02:00
|
|
|
ctrl := gomock.NewController(t)
|
|
|
|
|
|
|
|
policyCheckMock := tfe.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 := tfe.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
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
|
|
|
defer configCleanup()
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{})
|
|
|
|
|
|
|
|
op.AutoApprove = true
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 opertion 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, insead 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-22 23:53:33 +02:00
|
|
|
func TestCloud_applyPolicySoftFailAutoApprove(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
2021-09-22 23:53:33 +02:00
|
|
|
ctrl := gomock.NewController(t)
|
|
|
|
|
|
|
|
applyMock := tfe.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
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
// Create a named workspace that auto applies.
|
|
|
|
_, err := b.client.Workspaces.Create(
|
|
|
|
context.Background(),
|
|
|
|
b.organization,
|
|
|
|
tfe.WorkspaceCreateOptions{
|
2021-10-11 23:26:07 +02:00
|
|
|
Name: tfe.String("prod"),
|
2021-08-12 21:30:24 +02:00
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
|
|
|
input := testInput(t, map[string]string{
|
|
|
|
"override": "override",
|
|
|
|
"approve": "yes",
|
|
|
|
})
|
|
|
|
|
|
|
|
op.UIIn = input
|
|
|
|
op.UIOut = b.CLI
|
|
|
|
op.Workspace = "prod"
|
2021-09-22 23:53:33 +02:00
|
|
|
op.AutoApprove = true
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2021-09-22 23:53:33 +02:00
|
|
|
if len(input.answers) != 2 {
|
2021-08-12 21:30:24 +02:00
|
|
|
t.Fatalf("expected an unused answer, got: %v", input.answers)
|
|
|
|
}
|
|
|
|
|
|
|
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
|
|
|
if !strings.Contains(output, "Running apply in Terraform Cloud") {
|
|
|
|
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) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
defer bCleanup()
|
|
|
|
|
|
|
|
op, configCleanup, done := testOperationApply(t, "./testdata/apply-with-error")
|
|
|
|
defer configCleanup()
|
|
|
|
defer done(t)
|
|
|
|
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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_applyVersionCheck(t *testing.T) {
|
|
|
|
testCases := map[string]struct {
|
|
|
|
localVersion string
|
|
|
|
remoteVersion string
|
|
|
|
forceLocal bool
|
|
|
|
hasOperations bool
|
|
|
|
wantErr string
|
|
|
|
}{
|
|
|
|
"versions can be different for remote apply": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.13.5",
|
|
|
|
hasOperations: true,
|
|
|
|
},
|
|
|
|
"versions can be different for local apply": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.13.5",
|
|
|
|
hasOperations: false,
|
|
|
|
},
|
|
|
|
"force local with remote operations and different versions is acceptable": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.14.0-acme-provider-bundle",
|
|
|
|
forceLocal: true,
|
|
|
|
hasOperations: true,
|
|
|
|
},
|
|
|
|
"no error if versions are identical": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.14.0",
|
|
|
|
forceLocal: true,
|
|
|
|
hasOperations: true,
|
|
|
|
},
|
|
|
|
"no error if force local but workspace has remote operations disabled": {
|
|
|
|
localVersion: "0.14.0",
|
|
|
|
remoteVersion: "0.13.5",
|
|
|
|
forceLocal: true,
|
|
|
|
hasOperations: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, tc := range testCases {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
2021-09-20 23:54:41 +02:00
|
|
|
b, bCleanup := testBackendWithName(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
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,
|
2021-09-20 20:07:53 +02:00
|
|
|
b.WorkspaceMapping.Name,
|
2021-08-12 21:30:24 +02:00
|
|
|
tfe.WorkspaceUpdateOptions{
|
|
|
|
Operations: tfe.Bool(tc.hasOperations),
|
|
|
|
TerraformVersion: tfe.String(tc.remoteVersion),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating named workspace: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RUN: prepare the apply operation and run it
|
2021-09-23 00:12:51 +02:00
|
|
|
op, configCleanup, opDone := testOperationApply(t, "./testdata/apply")
|
2021-08-12 21:30:24 +02:00
|
|
|
defer configCleanup()
|
2021-09-23 00:12:51 +02:00
|
|
|
defer opDone(t)
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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
|
2021-09-20 20:07:53 +02:00
|
|
|
op.Workspace = testBackendSingleWorkspaceName
|
2021-08-12 21:30:24 +02:00
|
|
|
|
|
|
|
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 Terraform Cloud")
|
|
|
|
hasSummary := strings.Contains(output, "1 added, 0 changed, 0 destroyed")
|
2021-10-21 15:55:27 +02:00
|
|
|
hasResources := run.State.HasManagedResourceInstanceObjects()
|
2021-08-12 21:30:24 +02:00
|
|
|
if !tc.forceLocal && tc.hasOperations {
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-09-22 23:53:33 +02:00
|
|
|
|
|
|
|
const applySuccessOneResourceAdded = `
|
|
|
|
Terraform 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"
|
|
|
|
`
|