backend/remote: extend mocks and add apply tests
This commit is contained in:
parent
9f9bbcb0e7
commit
2bd1040bbd
|
@ -405,7 +405,6 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
|
||||||
"\n\nThe \"remote\" backend does not support the %q operation.\n"+
|
"\n\nThe \"remote\" backend does not support the %q operation.\n"+
|
||||||
"Please use the remote backend web UI for running this operation:\n"+
|
"Please use the remote backend web UI for running this operation:\n"+
|
||||||
"https://%s/app/%s/%s", op.Type, b.hostname, b.organization, op.Workspace)
|
"https://%s/app/%s/%s", op.Type, b.hostname, b.organization, op.Workspace)
|
||||||
// return nil, backend.ErrOperationNotSupported
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock
|
// Lock
|
||||||
|
|
|
@ -91,9 +91,9 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
|
||||||
fmt.Sprint(applyErrNoApplyRights, b.hostname, b.organization, op.Workspace)))
|
fmt.Sprint(applyErrNoApplyRights, b.hostname, b.organization, op.Workspace)))
|
||||||
}
|
}
|
||||||
|
|
||||||
hasUI := op.UIOut != nil && op.UIIn != nil
|
hasUI := op.UIIn != nil && op.UIOut != nil
|
||||||
mustConfirm := hasUI &&
|
mustConfirm := hasUI &&
|
||||||
(op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove)
|
((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove))
|
||||||
if mustConfirm {
|
if mustConfirm {
|
||||||
opts := &terraform.InputOpts{Id: "approve"}
|
opts := &terraform.InputOpts{Id: "approve"}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,408 @@
|
||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testOperationApply() *backend.Operation {
|
||||||
|
return &backend.Operation{
|
||||||
|
Type: backend.OperationTypeApply,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyBasic(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
input := testInput(t, map[string]string{
|
||||||
|
"approve": "yes",
|
||||||
|
})
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Module = mod
|
||||||
|
op.UIIn = input
|
||||||
|
op.UIOut = b.CLI
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err != nil {
|
||||||
|
t.Fatalf("error running operation: %v", run.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "1 to add, 0 to change, 0 to destroy") {
|
||||||
|
t.Fatalf("missing plan summery in output: %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||||
|
t.Fatalf("missing apply summery in output: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyWithVCS(t *testing.T) {
|
||||||
|
b := testBackendNoDefault(t)
|
||||||
|
|
||||||
|
// Create the named workspace with a VCS.
|
||||||
|
_, err := b.client.Workspaces.Create(
|
||||||
|
context.Background(),
|
||||||
|
b.organization,
|
||||||
|
tfe.WorkspaceCreateOptions{
|
||||||
|
Name: tfe.String(b.prefix + "prod"),
|
||||||
|
VCSRepo: &tfe.VCSRepoOptions{},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating named workspace: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Module = mod
|
||||||
|
op.Workspace = "prod"
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
<-run.Done()
|
||||||
|
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a apply error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "not allowed for workspaces with a VCS") {
|
||||||
|
t.Fatalf("expected a VCS error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyWithPlan(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Module = mod
|
||||||
|
op.Plan = &terraform.Plan{}
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
<-run.Done()
|
||||||
|
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a apply error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "saved plan is currently not supported") {
|
||||||
|
t.Fatalf("expected a saved plan error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyWithTarget(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Module = mod
|
||||||
|
op.Targets = []string{"null_resource.foo"}
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
<-run.Done()
|
||||||
|
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a apply error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "targeting is currently not supported") {
|
||||||
|
t.Fatalf("expected a targeting error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyNoConfig(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Module = nil
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
<-run.Done()
|
||||||
|
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a apply error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "configuration files found") {
|
||||||
|
t.Fatalf("expected configuration files error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyNoChanges(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-no-changes")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Module = mod
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err != nil {
|
||||||
|
t.Fatalf("error running operation: %v", run.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyNoApprove(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
input := testInput(t, map[string]string{
|
||||||
|
"approve": "no",
|
||||||
|
})
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Module = mod
|
||||||
|
op.UIIn = input
|
||||||
|
op.UIOut = b.CLI
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a apply error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "Apply discarded") {
|
||||||
|
t.Fatalf("expected a apply discarded error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if len(input.answers) > 0 {
|
||||||
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyAutoApprove(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
input := testInput(t, map[string]string{
|
||||||
|
"approve": "no",
|
||||||
|
})
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.AutoApprove = true
|
||||||
|
op.Module = mod
|
||||||
|
op.UIIn = input
|
||||||
|
op.UIOut = b.CLI
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err != nil {
|
||||||
|
t.Fatalf("error running operation: %v", run.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "1 to add, 0 to change, 0 to destroy") {
|
||||||
|
t.Fatalf("missing plan summery in output: %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
|
||||||
|
t.Fatalf("missing apply summery in output: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyLockTimeout(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Retrieve the workspace used to run this operation in.
|
||||||
|
w, err := b.client.Workspaces.Read(ctx, b.organization, b.workspace)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
input := testInput(t, map[string]string{
|
||||||
|
"approve": "yes",
|
||||||
|
})
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.StateLockTimeout = 5 * time.Second
|
||||||
|
op.Module = mod
|
||||||
|
op.UIIn = input
|
||||||
|
op.UIOut = b.CLI
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
_, 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(10 * time.Second):
|
||||||
|
t.Fatalf("expected lock timeout after 5 seconds, waited 10 seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "Lock timeout exceeded") {
|
||||||
|
t.Fatalf("missing 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 TestRemote_applyDestroy(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-destroy")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
input := testInput(t, map[string]string{
|
||||||
|
"approve": "yes",
|
||||||
|
})
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Destroy = true
|
||||||
|
op.Module = mod
|
||||||
|
op.UIIn = input
|
||||||
|
op.UIOut = b.CLI
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err != nil {
|
||||||
|
t.Fatalf("error running operation: %v", run.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "0 to add, 0 to change, 1 to destroy") {
|
||||||
|
t.Fatalf("missing plan summery in output: %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") {
|
||||||
|
t.Fatalf("missing apply summery in output: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_applyDestroyNoConfig(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
input := testInput(t, map[string]string{
|
||||||
|
"approve": "yes",
|
||||||
|
})
|
||||||
|
|
||||||
|
op := testOperationApply()
|
||||||
|
op.Destroy = true
|
||||||
|
op.Module = nil
|
||||||
|
op.UIIn = input
|
||||||
|
op.UIOut = b.CLI
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err != nil {
|
||||||
|
t.Fatalf("unexpected apply error: %v", run.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input.answers) > 0 {
|
||||||
|
t.Fatalf("expected no unused answers, got: %v", input.answers)
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,11 +12,14 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
tfe "github.com/hashicorp/go-tfe"
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockClient struct {
|
type mockClient struct {
|
||||||
|
Applies *mockApplies
|
||||||
ConfigurationVersions *mockConfigurationVersions
|
ConfigurationVersions *mockConfigurationVersions
|
||||||
Organizations *mockOrganizations
|
Organizations *mockOrganizations
|
||||||
Plans *mockPlans
|
Plans *mockPlans
|
||||||
|
@ -27,6 +30,7 @@ type mockClient struct {
|
||||||
|
|
||||||
func newMockClient() *mockClient {
|
func newMockClient() *mockClient {
|
||||||
c := &mockClient{}
|
c := &mockClient{}
|
||||||
|
c.Applies = newMockApplies(c)
|
||||||
c.ConfigurationVersions = newMockConfigurationVersions(c)
|
c.ConfigurationVersions = newMockConfigurationVersions(c)
|
||||||
c.Organizations = newMockOrganizations(c)
|
c.Organizations = newMockOrganizations(c)
|
||||||
c.Plans = newMockPlans(c)
|
c.Plans = newMockPlans(c)
|
||||||
|
@ -36,6 +40,106 @@ func newMockClient() *mockClient {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockApplies struct {
|
||||||
|
client *mockClient
|
||||||
|
applies map[string]*tfe.Apply
|
||||||
|
logs map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockApplies(client *mockClient) *mockApplies {
|
||||||
|
return &mockApplies{
|
||||||
|
client: client,
|
||||||
|
applies: make(map[string]*tfe.Apply),
|
||||||
|
logs: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create is a helper function to create a mock apply that uses the configured
|
||||||
|
// working directory to find the logfile. This enables us to test if we are
|
||||||
|
// using the
|
||||||
|
func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) {
|
||||||
|
c, ok := m.client.ConfigurationVersions.configVersions[cvID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
if c.Speculative {
|
||||||
|
// Speculative means its plan-only so we don't create a Apply.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
id := generateID("apply-")
|
||||||
|
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
|
||||||
|
|
||||||
|
a := &tfe.Apply{
|
||||||
|
ID: id,
|
||||||
|
LogReadURL: url,
|
||||||
|
Status: tfe.ApplyPending,
|
||||||
|
}
|
||||||
|
|
||||||
|
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logs[url] = filepath.Join(
|
||||||
|
m.client.ConfigurationVersions.uploadPaths[cvID],
|
||||||
|
w.WorkingDirectory,
|
||||||
|
"apply.log",
|
||||||
|
)
|
||||||
|
m.applies[a.ID] = a
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) {
|
||||||
|
a, ok := m.applies[applyID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
// Together with the mockLogReader this allows testing queued runs.
|
||||||
|
if a.Status == tfe.ApplyRunning {
|
||||||
|
a.Status = tfe.ApplyFinished
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) {
|
||||||
|
a, err := m.Read(ctx, applyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logfile, ok := m.logs[a.LogReadURL]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(logfile); os.IsNotExist(err) {
|
||||||
|
return bytes.NewBufferString("logfile does not exist"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := ioutil.ReadFile(logfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
done := func() (bool, error) {
|
||||||
|
a, err := m.Read(ctx, applyID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if a.Status != tfe.ApplyFinished {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mockLogReader{
|
||||||
|
done: done,
|
||||||
|
logs: bytes.NewBuffer(logs),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type mockConfigurationVersions struct {
|
type mockConfigurationVersions struct {
|
||||||
client *mockClient
|
client *mockClient
|
||||||
configVersions map[string]*tfe.ConfigurationVersion
|
configVersions map[string]*tfe.ConfigurationVersion
|
||||||
|
@ -103,6 +207,20 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mockInput is a mock implementation of terraform.UIInput.
|
||||||
|
type mockInput struct {
|
||||||
|
answers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockInput) Input(opts *terraform.InputOpts) (string, error) {
|
||||||
|
v, ok := m.answers[opts.Id]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
|
||||||
|
}
|
||||||
|
delete(m.answers, opts.Id)
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
type mockOrganizations struct {
|
type mockOrganizations struct {
|
||||||
client *mockClient
|
client *mockClient
|
||||||
organizations map[string]*tfe.Organization
|
organizations map[string]*tfe.Organization
|
||||||
|
@ -132,6 +250,32 @@ func (m *mockOrganizations) List(ctx context.Context, options tfe.OrganizationLi
|
||||||
return orgl, nil
|
return orgl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mockLogReader is a mock logreader that enables testing queued runs.
|
||||||
|
type mockLogReader struct {
|
||||||
|
done func() (bool, error)
|
||||||
|
logs *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLogReader) Read(l []byte) (int, error) {
|
||||||
|
for {
|
||||||
|
if written, err := m.read(l); err != io.ErrNoProgress {
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLogReader) read(l []byte) (int, error) {
|
||||||
|
done, err := m.done()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if !done {
|
||||||
|
return 0, io.ErrNoProgress
|
||||||
|
}
|
||||||
|
return m.logs.Read(l)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) {
|
func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) {
|
||||||
org := &tfe.Organization{Name: *options.Name}
|
org := &tfe.Organization{Name: *options.Name}
|
||||||
m.organizations[org.Name] = org
|
m.organizations[org.Name] = org
|
||||||
|
@ -196,7 +340,7 @@ func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) {
|
||||||
m.logs[url] = filepath.Join(
|
m.logs[url] = filepath.Join(
|
||||||
m.client.ConfigurationVersions.uploadPaths[cvID],
|
m.client.ConfigurationVersions.uploadPaths[cvID],
|
||||||
w.WorkingDirectory,
|
w.WorkingDirectory,
|
||||||
"output.log",
|
"plan.log",
|
||||||
)
|
)
|
||||||
m.plans[p.ID] = p
|
m.plans[p.ID] = p
|
||||||
|
|
||||||
|
@ -208,7 +352,10 @@ func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, tfe.ErrResourceNotFound
|
return nil, tfe.ErrResourceNotFound
|
||||||
}
|
}
|
||||||
|
// Together with the mockLogReader this allows testing queued runs.
|
||||||
|
if p.Status == tfe.PlanRunning {
|
||||||
p.Status = tfe.PlanFinished
|
p.Status = tfe.PlanFinished
|
||||||
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +379,21 @@ func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes.NewBuffer(logs), nil
|
done := func() (bool, error) {
|
||||||
|
p, err := m.Read(ctx, planID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if p.Status != tfe.PlanFinished {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mockLogReader{
|
||||||
|
done: done,
|
||||||
|
logs: bytes.NewBuffer(logs),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockRuns struct {
|
type mockRuns struct {
|
||||||
|
@ -272,6 +433,11 @@ func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.Run
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
|
func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
|
||||||
|
a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID)
|
p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -279,10 +445,25 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
|
||||||
|
|
||||||
r := &tfe.Run{
|
r := &tfe.Run{
|
||||||
ID: generateID("run-"),
|
ID: generateID("run-"),
|
||||||
|
Actions: &tfe.RunActions{},
|
||||||
|
Apply: a,
|
||||||
|
HasChanges: true,
|
||||||
|
Permissions: &tfe.RunPermissions{},
|
||||||
Plan: p,
|
Plan: p,
|
||||||
Status: tfe.RunPending,
|
Status: tfe.RunPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.IsDestroy != nil {
|
||||||
|
r.IsDestroy = *options.IsDestroy
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, _ := ioutil.ReadFile(m.client.Plans.logs[p.LogReadURL])
|
||||||
|
if r.IsDestroy || !bytes.Contains(logs, []byte("No changes. Infrastructure is up-to-date.")) {
|
||||||
|
r.Actions.IsConfirmable = true
|
||||||
|
r.HasChanges = true
|
||||||
|
r.Permissions.CanApply = true
|
||||||
|
}
|
||||||
|
|
||||||
m.runs[r.ID] = r
|
m.runs[r.ID] = r
|
||||||
m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r)
|
m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r)
|
||||||
|
|
||||||
|
@ -294,11 +475,35 @@ func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, tfe.ErrResourceNotFound
|
return nil, tfe.ErrResourceNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pending := false
|
||||||
|
for _, r := range m.runs {
|
||||||
|
if r.ID != runID && r.Status == tfe.RunPending {
|
||||||
|
pending = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pending {
|
||||||
|
// Only update the status if there are no other pending runs.
|
||||||
|
r.Status = tfe.RunPlanning
|
||||||
|
r.Plan.Status = tfe.PlanRunning
|
||||||
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
|
func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
|
||||||
panic("not implemented")
|
r, ok := m.runs[runID]
|
||||||
|
if !ok {
|
||||||
|
return tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
if r.Status != tfe.RunPending {
|
||||||
|
// Only update the status if the run is not pending anymore.
|
||||||
|
r.Status = tfe.RunApplying
|
||||||
|
r.Apply.Status = tfe.ApplyRunning
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error {
|
func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error {
|
||||||
|
@ -480,6 +685,9 @@ func (m *mockWorkspaces) Create(ctx context.Context, organization string, option
|
||||||
ID: generateID("ws-"),
|
ID: generateID("ws-"),
|
||||||
Name: *options.Name,
|
Name: *options.Name,
|
||||||
}
|
}
|
||||||
|
if options.VCSRepo != nil {
|
||||||
|
w.VCSRepo = &tfe.VCSRepo{}
|
||||||
|
}
|
||||||
m.workspaceIDs[w.ID] = w
|
m.workspaceIDs[w.ID] = w
|
||||||
m.workspaceNames[w.Name] = w
|
m.workspaceNames[w.Name] = w
|
||||||
return w, nil
|
return w, nil
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Do you really want to destroy all resources in workspace "my-app-dev"?
|
||||||
|
Terraform will destroy all your managed infrastructure, as shown above.
|
||||||
|
There is no undo. Only 'yes' will be accepted to confirm.
|
||||||
|
|
||||||
|
Enter a value: yes
|
||||||
|
|
||||||
|
null_resource.hello: Destroying... (ID: 8657651096157629581)
|
||||||
|
null_resource.hello: Destruction complete after 0s
|
||||||
|
|
||||||
|
Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
|
|
@ -0,0 +1 @@
|
||||||
|
resource "null_resource" "foo" {}
|
|
@ -0,0 +1,22 @@
|
||||||
|
Terraform v0.11.7
|
||||||
|
|
||||||
|
Configuring remote state backend...
|
||||||
|
Initializing Terraform configuration...
|
||||||
|
Refreshing Terraform state in-memory prior to plan...
|
||||||
|
The refreshed state will be used to calculate this plan, but will not be
|
||||||
|
persisted to local or remote state storage.
|
||||||
|
|
||||||
|
null_resource.hello: Refreshing state... (ID: 8657651096157629581)
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
An execution plan has been generated and is shown below.
|
||||||
|
Resource actions are indicated with the following symbols:
|
||||||
|
- destroy
|
||||||
|
|
||||||
|
Terraform will perform the following actions:
|
||||||
|
|
||||||
|
- null_resource.hello
|
||||||
|
|
||||||
|
|
||||||
|
Plan: 0 to add, 0 to change, 1 to destroy.
|
|
@ -0,0 +1 @@
|
||||||
|
resource "null_resource" "foo" {}
|
|
@ -0,0 +1,17 @@
|
||||||
|
Terraform v0.11.7
|
||||||
|
|
||||||
|
Configuring remote state backend...
|
||||||
|
Initializing Terraform configuration...
|
||||||
|
Refreshing Terraform state in-memory prior to plan...
|
||||||
|
The refreshed state will be used to calculate this plan, but will not be
|
||||||
|
persisted to local or remote state storage.
|
||||||
|
|
||||||
|
null_resource.hello: Refreshing state... (ID: 8657651096157629581)
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
No changes. Infrastructure is up-to-date.
|
||||||
|
|
||||||
|
This means that Terraform did not detect any differences between your
|
||||||
|
configuration and real physical resources that exist. As a result, no
|
||||||
|
actions need to be performed.
|
|
@ -0,0 +1,12 @@
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Do you want to perform these actions in workspace "my-workspace-name"?
|
||||||
|
Terraform will perform the actions described above.
|
||||||
|
Only 'yes' will be accepted to approve.
|
||||||
|
|
||||||
|
Enter a value: yes
|
||||||
|
|
||||||
|
null_resource.hello: Creating...
|
||||||
|
null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
|
||||||
|
|
||||||
|
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
|
@ -0,0 +1 @@
|
||||||
|
resource "null_resource" "foo" {}
|
|
@ -1,10 +1,3 @@
|
||||||
Running plan in the remote backend. Output will stream here. Pressing Ctrl-C
|
|
||||||
will stop streaming the logs, but will not stop the plan running remotely.
|
|
||||||
To view this plan in a browser, visit:
|
|
||||||
https://atlas.local/app/demo1/my-app-web/runs/run-cPK6EnfTpqwy6ucU
|
|
||||||
|
|
||||||
Waiting for the plan to start...
|
|
||||||
|
|
||||||
Terraform v0.11.7
|
Terraform v0.11.7
|
||||||
|
|
||||||
Configuring remote state backend...
|
Configuring remote state backend...
|
||||||
|
@ -13,7 +6,6 @@ Refreshing Terraform state in-memory prior to plan...
|
||||||
The refreshed state will be used to calculate this plan, but will not be
|
The refreshed state will be used to calculate this plan, but will not be
|
||||||
persisted to local or remote state storage.
|
persisted to local or remote state storage.
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
An execution plan has been generated and is shown below.
|
An execution plan has been generated and is shown below.
|
|
@ -0,0 +1,16 @@
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Organization policy check:
|
||||||
|
|
||||||
|
Sentinel Result: true
|
||||||
|
|
||||||
|
This result means that Sentinel policies returned true and the protected
|
||||||
|
behavior is allowed by Sentinel policies.
|
||||||
|
|
||||||
|
1 policies evaluated.
|
||||||
|
|
||||||
|
## Policy 1: Passthrough.sentinel (soft-mandatory)
|
||||||
|
|
||||||
|
Result: true
|
||||||
|
|
||||||
|
TRUE - Passthrough.sentinel:1:1 - Rule "main"
|
|
@ -1,10 +1,3 @@
|
||||||
Running plan in the remote backend. Output will stream here. Pressing Ctrl-C
|
|
||||||
will stop streaming the logs, but will not stop the plan running remotely.
|
|
||||||
To view this plan in a browser, visit:
|
|
||||||
https://atlas.local/app/demo1/my-app-web/runs/run-cPK6EnfTpqwy6ucU
|
|
||||||
|
|
||||||
Waiting for the plan to start...
|
|
||||||
|
|
||||||
Terraform v0.11.7
|
Terraform v0.11.7
|
||||||
|
|
||||||
Configuring remote state backend...
|
Configuring remote state backend...
|
||||||
|
@ -13,7 +6,6 @@ Refreshing Terraform state in-memory prior to plan...
|
||||||
The refreshed state will be used to calculate this plan, but will not be
|
The refreshed state will be used to calculate this plan, but will not be
|
||||||
persisted to local or remote state storage.
|
persisted to local or remote state storage.
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
An execution plan has been generated and is shown below.
|
An execution plan has been generated and is shown below.
|
|
@ -0,0 +1,21 @@
|
||||||
|
Terraform v0.11.7
|
||||||
|
|
||||||
|
Configuring remote state backend...
|
||||||
|
Initializing Terraform configuration...
|
||||||
|
Refreshing Terraform state in-memory prior to plan...
|
||||||
|
The refreshed state will be used to calculate this plan, but will not be
|
||||||
|
persisted to local or remote state storage.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
An execution plan has been generated and is shown below.
|
||||||
|
Resource actions are indicated with the following symbols:
|
||||||
|
+ create
|
||||||
|
|
||||||
|
Terraform will perform the following actions:
|
||||||
|
|
||||||
|
+ null_resource.foo
|
||||||
|
id: <computed>
|
||||||
|
|
||||||
|
|
||||||
|
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
@ -28,6 +28,10 @@ var (
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func testInput(t *testing.T, answers map[string]string) *mockInput {
|
||||||
|
return &mockInput{answers: answers}
|
||||||
|
}
|
||||||
|
|
||||||
func testBackendDefault(t *testing.T) *Remote {
|
func testBackendDefault(t *testing.T) *Remote {
|
||||||
c := map[string]interface{}{
|
c := map[string]interface{}{
|
||||||
"organization": "hashicorp",
|
"organization": "hashicorp",
|
||||||
|
@ -74,6 +78,7 @@ func testBackend(t *testing.T, c map[string]interface{}) *Remote {
|
||||||
|
|
||||||
// Replace the services we use with our mock services.
|
// Replace the services we use with our mock services.
|
||||||
b.CLI = cli.NewMockUi()
|
b.CLI = cli.NewMockUi()
|
||||||
|
b.client.Applies = mc.Applies
|
||||||
b.client.ConfigurationVersions = mc.ConfigurationVersions
|
b.client.ConfigurationVersions = mc.ConfigurationVersions
|
||||||
b.client.Organizations = mc.Organizations
|
b.client.Organizations = mc.Organizations
|
||||||
b.client.Plans = mc.Plans
|
b.client.Plans = mc.Plans
|
||||||
|
|
Loading…
Reference in New Issue