backend/remote: extend mocks and add apply tests

This commit is contained in:
Sander van Harmelen 2018-09-26 17:27:12 +02:00
parent 9f9bbcb0e7
commit 2bd1040bbd
16 changed files with 733 additions and 26 deletions

View File

@ -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

View File

@ -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"}

View File

@ -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)
}
}

View File

@ -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
} }
p.Status = tfe.PlanFinished // Together with the mockLogReader this allows testing queued runs.
if p.Status == tfe.PlanRunning {
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,15 +433,35 @@ 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
} }
r := &tfe.Run{ r := &tfe.Run{
ID: generateID("run-"), ID: generateID("run-"),
Plan: p, Actions: &tfe.RunActions{},
Status: tfe.RunPending, Apply: a,
HasChanges: true,
Permissions: &tfe.RunPermissions{},
Plan: p,
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
@ -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

View File

@ -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.

View File

@ -0,0 +1 @@
resource "null_resource" "foo" {}

View File

@ -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.

View File

@ -0,0 +1 @@
resource "null_resource" "foo" {}

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1 @@
resource "null_resource" "foo" {}

View File

@ -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.

View File

@ -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"

View File

@ -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.

View File

@ -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.

View File

@ -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