Merge pull request #19022 from hashicorp/f-auto-apply

backend/remote: properly handle workspaces that auto apply changes
This commit is contained in:
Sander van Harmelen 2018-10-09 09:43:25 +02:00 committed by GitHub
commit 53a8aaaf85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 167 additions and 41 deletions

View File

@ -90,14 +90,14 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
} }
// Return if the run cannot be confirmed. // Return if the run cannot be confirmed.
if !r.Actions.IsConfirmable { if !w.AutoApply && !r.Actions.IsConfirmable {
return r, nil return r, nil
} }
// Since we already checked the permissions before creating the run // Since we already checked the permissions before creating the run
// this should never happen. But it doesn't hurt to keep this in as // this should never happen. But it doesn't hurt to keep this in as
// a safeguard for any unexpected situations. // a safeguard for any unexpected situations.
if !r.Permissions.CanApply { if !w.AutoApply && !r.Permissions.CanApply {
// Make sure we discard the run if possible. // Make sure we discard the run if possible.
if r.Actions.IsDiscardable { if r.Actions.IsDiscardable {
err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{})
@ -112,35 +112,40 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
fmt.Sprintf(applyErrNoApplyRights, b.hostname, b.organization, op.Workspace))) fmt.Sprintf(applyErrNoApplyRights, b.hostname, b.organization, op.Workspace)))
} }
hasUI := op.UIIn != nil && op.UIOut != nil mustConfirm := (op.UIIn != nil && op.UIOut != nil) &&
mustConfirm := hasUI &&
((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove)) ((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove))
if mustConfirm {
opts := &terraform.InputOpts{Id: "approve"}
if op.Destroy { if !w.AutoApply {
opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" if mustConfirm {
opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" + opts := &terraform.InputOpts{Id: "approve"}
"There is no undo. Only 'yes' will be accepted to confirm."
} else { if op.Destroy {
opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?" opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
opts.Description = "Terraform will perform the actions described above.\n" + opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
"Only 'yes' will be accepted to approve." "There is no undo. Only 'yes' will be accepted to confirm."
} else {
opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?"
opts.Description = "Terraform will perform the actions described above.\n" +
"Only 'yes' will be accepted to approve."
}
if err = b.confirm(stopCtx, op, opts, r, "yes"); err != nil {
return r, err
}
} }
if err = b.confirm(stopCtx, op, opts, r, "yes"); err != nil { err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{})
return r, err if err != nil {
} return r, generalError("error approving the apply command", err)
} else {
if b.CLI != nil {
// Insert a blank line to separate the ouputs.
b.CLI.Output("")
} }
} }
err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}) // If we don't need to ask for confirmation, insert a blank
if err != nil { // line to separate the ouputs.
return r, generalError("error approving the apply command", err) if w.AutoApply || !mustConfirm {
if b.CLI != nil {
b.CLI.Output("")
}
} }
logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID) logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID)

View File

@ -66,6 +66,61 @@ func TestRemote_applyBasic(t *testing.T) {
} }
} }
func TestRemote_applyWithAutoApply(t *testing.T) {
b := testBackendNoDefault(t)
// Create a named workspace that auto applies.
_, err := b.client.Workspaces.Create(
context.Background(),
b.organization,
tfe.WorkspaceCreateOptions{
AutoApply: tfe.Bool(true),
Name: tfe.String(b.prefix + "prod"),
},
)
if err != nil {
t.Fatalf("error creating named workspace: %v", err)
}
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 = "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("error running operation: %v", run.Err)
}
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, "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_applyWithoutPermissions(t *testing.T) { func TestRemote_applyWithoutPermissions(t *testing.T) {
b := testBackendNoDefault(t) b := testBackendNoDefault(t)
@ -93,8 +148,8 @@ func TestRemote_applyWithoutPermissions(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err) t.Fatalf("expected an apply error, got: %v", run.Err)
} }
@ -130,8 +185,8 @@ func TestRemote_applyWithVCS(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err) t.Fatalf("expected an apply error, got: %v", run.Err)
} }
@ -158,8 +213,8 @@ func TestRemote_applyWithParallelism(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err) t.Fatalf("expected an apply error, got: %v", run.Err)
} }
@ -183,8 +238,8 @@ func TestRemote_applyWithPlan(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err) t.Fatalf("expected an apply error, got: %v", run.Err)
} }
@ -211,8 +266,8 @@ func TestRemote_applyWithoutRefresh(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err) t.Fatalf("expected an apply error, got: %v", run.Err)
} }
@ -236,8 +291,8 @@ func TestRemote_applyWithTarget(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err) t.Fatalf("expected an apply error, got: %v", run.Err)
} }
@ -264,8 +319,8 @@ func TestRemote_applyWithVariables(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err) t.Fatalf("expected an apply error, got: %v", run.Err)
} }
@ -285,8 +340,8 @@ func TestRemote_applyNoConfig(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an apply error, got: %v", run.Err) t.Fatalf("expected an apply error, got: %v", run.Err)
} }
@ -690,6 +745,65 @@ func TestRemote_applyPolicySoftFail(t *testing.T) {
} }
} }
func TestRemote_applyPolicySoftFailAutoApply(t *testing.T) {
b := testBackendDefault(t)
// Create a named workspace that auto applies.
_, err := b.client.Workspaces.Create(
context.Background(),
b.organization,
tfe.WorkspaceCreateOptions{
AutoApply: tfe.Bool(true),
Name: tfe.String(b.prefix + "prod"),
},
)
if err != nil {
t.Fatalf("error creating named workspace: %v", err)
}
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-policy-soft-failed")
defer modCleanup()
input := testInput(t, map[string]string{
"override": "override",
"approve": "yes",
})
op := testOperationApply()
op.Module = mod
op.UIIn = input
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()
if run.Err != nil {
t.Fatalf("error running operation: %v", run.Err)
}
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, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("missing plan summery in output: %s", output)
}
if !strings.Contains(output, "Sentinel Result: false") {
t.Fatalf("missing policy check result 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_applyPolicySoftFailAutoApprove(t *testing.T) { func TestRemote_applyPolicySoftFailAutoApprove(t *testing.T) {
b := testBackendDefault(t) b := testBackendDefault(t)

View File

@ -82,6 +82,10 @@ func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) {
return nil, tfe.ErrResourceNotFound return nil, tfe.ErrResourceNotFound
} }
if w.AutoApply {
a.Status = tfe.ApplyRunning
}
m.logs[url] = filepath.Join( m.logs[url] = filepath.Join(
m.client.ConfigurationVersions.uploadPaths[cvID], m.client.ConfigurationVersions.uploadPaths[cvID],
w.WorkingDirectory, w.WorkingDirectory,
@ -866,6 +870,9 @@ func (m *mockWorkspaces) Create(ctx context.Context, organization string, option
CanUpdate: true, CanUpdate: true,
}, },
} }
if options.AutoApply != nil {
w.AutoApply = *options.AutoApply
}
if options.VCSRepo != nil { if options.VCSRepo != nil {
w.VCSRepo = &tfe.VCSRepo{} w.VCSRepo = &tfe.VCSRepo{}
} }

View File

@ -81,8 +81,8 @@ func TestRemote_planWithoutPermissions(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err) t.Fatalf("expected a plan error, got: %v", run.Err)
} }
@ -106,8 +106,8 @@ func TestRemote_planWithModuleDepth(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err) t.Fatalf("expected a plan error, got: %v", run.Err)
} }
@ -131,8 +131,8 @@ func TestRemote_planWithParallelism(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err) t.Fatalf("expected a plan error, got: %v", run.Err)
} }
@ -156,8 +156,8 @@ func TestRemote_planWithPlan(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err) t.Fatalf("expected a plan error, got: %v", run.Err)
} }
@ -184,8 +184,8 @@ func TestRemote_planWithPath(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err) t.Fatalf("expected a plan error, got: %v", run.Err)
} }
@ -212,8 +212,8 @@ func TestRemote_planWithoutRefresh(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err) t.Fatalf("expected a plan error, got: %v", run.Err)
} }
@ -237,8 +237,8 @@ func TestRemote_planWithTarget(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err) t.Fatalf("expected a plan error, got: %v", run.Err)
} }
@ -265,8 +265,8 @@ func TestRemote_planWithVariables(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected an plan error, got: %v", run.Err) t.Fatalf("expected an plan error, got: %v", run.Err)
} }
@ -286,8 +286,8 @@ func TestRemote_planNoConfig(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error starting operation: %v", err) t.Fatalf("error starting operation: %v", err)
} }
<-run.Done()
<-run.Done()
if run.Err == nil { if run.Err == nil {
t.Fatalf("expected a plan error, got: %v", run.Err) t.Fatalf("expected a plan error, got: %v", run.Err)
} }