cli: Remove deprecated destroy -force flag

This dramatically simplifies the logic around auto-approve, which is
nice.

Also add test coverage for the manual approve step, for both apply and
destroy, answering both yes and no.
This commit is contained in:
Alisdair McDiarmid 2021-02-03 15:05:05 -05:00
parent 030632e87e
commit b2ba650c21
6 changed files with 211 additions and 16 deletions

View File

@ -188,12 +188,11 @@ type Operation struct {
// The options below are more self-explanatory and affect the runtime
// behavior of the operation.
AutoApprove bool
Destroy bool
DestroyForce bool
Parallelism int
Targets []addrs.Targetable
Variables map[string]UnparsedVariableValue
AutoApprove bool
Destroy bool
Parallelism int
Targets []addrs.Targetable
Variables map[string]UnparsedVariableValue
// Some operations use root module variables only opportunistically or
// don't need them at all. If this flag is set, the backend must treat

View File

@ -79,7 +79,7 @@ func (b *Local) opApply(
trivialPlan := plan.Changes.Empty()
hasUI := op.UIOut != nil && op.UIIn != nil
mustConfirm := hasUI && ((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove && !trivialPlan))
mustConfirm := hasUI && !op.AutoApprove && !trivialPlan
if mustConfirm {
var desc, query string
if op.Destroy {

View File

@ -170,8 +170,7 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
return r, diags.Err()
}
mustConfirm := (op.UIIn != nil && op.UIOut != nil) &&
((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove))
mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove
if !w.AutoApply {
if mustConfirm {

View File

@ -24,7 +24,7 @@ type ApplyCommand struct {
}
func (c *ApplyCommand) Run(args []string) int {
var destroyForce, refresh, autoApprove bool
var refresh, autoApprove bool
args = c.Meta.process(args)
cmdName := "apply"
if c.Destroy {
@ -33,9 +33,6 @@ func (c *ApplyCommand) Run(args []string) int {
cmdFlags := c.Meta.extendedFlagSet(cmdName)
cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of plan before applying")
if c.Destroy {
cmdFlags.BoolVar(&destroyForce, "force", false, "deprecated: same as auto-approve")
}
cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
@ -160,7 +157,6 @@ func (c *ApplyCommand) Run(args []string) int {
opReq.AutoApprove = autoApprove
opReq.ConfigDir = configPath
opReq.Destroy = c.Destroy
opReq.DestroyForce = destroyForce
opReq.PlanFile = planFile
opReq.PlanRefresh = refresh
opReq.Type = backend.OperationTypeApply
@ -319,8 +315,6 @@ Options:
-auto-approve Skip interactive approval before destroying.
-force Deprecated: same as auto-approve.
-lock=true Lock the state file when locking is supported.
-lock-timeout=0s Duration to retry a state lock.

View File

@ -1,6 +1,7 @@
package command
import (
"bytes"
"os"
"strings"
"testing"
@ -114,6 +115,123 @@ func TestApply_destroy(t *testing.T) {
}
}
func TestApply_destroyApproveNo(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Create some existing state
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"bar"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
statePath := testStateFile(t, originalState)
// Disable test mode so input would be asked
test = false
defer func() { test = true }()
// Answer approval request with "no"
defaultInputReader = bytes.NewBufferString("no\n")
defaultInputWriter = new(bytes.Buffer)
p := applyFixtureProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if got, want := ui.OutputWriter.String(), "Destroy cancelled"; !strings.Contains(got, want) {
t.Fatalf("expected output to include %q, but was:\n%s", want, got)
}
state := testStateRead(t, statePath)
if state == nil {
t.Fatal("state should not be nil")
}
actualStr := strings.TrimSpace(state.String())
expectedStr := strings.TrimSpace(originalState.String())
if actualStr != expectedStr {
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
}
}
func TestApply_destroyApproveYes(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
statePath := testTempFile(t)
p := applyFixtureProvider()
// Disable test mode so input would be asked
test = false
defer func() { test = true }()
// Answer approval request with "yes"
defaultInputReader = bytes.NewBufferString("yes\n")
defaultInputWriter = new(bytes.Buffer)
ui := new(cli.MockUi)
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if _, err := os.Stat(statePath); err != nil {
t.Fatalf("err: %s", err)
}
state := testStateRead(t, statePath)
if state == nil {
t.Fatal("state should not be nil")
}
actualStr := strings.TrimSpace(state.String())
expectedStr := strings.TrimSpace(testApplyDestroyStr)
if actualStr != expectedStr {
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
}
}
func TestApply_destroyLockedState(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(

View File

@ -63,6 +63,91 @@ func TestApply(t *testing.T) {
}
}
func TestApply_approveNo(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
statePath := testTempFile(t)
// Disable test mode so input would be asked
test = false
defer func() { test = true }()
// Answer approval request with "no"
defaultInputReader = bytes.NewBufferString("no\n")
defaultInputWriter = new(bytes.Buffer)
p := applyFixtureProvider()
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if got, want := ui.OutputWriter.String(), "Apply cancelled"; !strings.Contains(got, want) {
t.Fatalf("expected output to include %q, but was:\n%s", want, got)
}
if _, err := os.Stat(statePath); err == nil || !os.IsNotExist(err) {
t.Fatalf("state file should not exist")
}
}
func TestApply_approveYes(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
statePath := testTempFile(t)
p := applyFixtureProvider()
// Disable test mode so input would be asked
test = false
defer func() { test = true }()
// Answer approval request with "yes"
defaultInputReader = bytes.NewBufferString("yes\n")
defaultInputWriter = new(bytes.Buffer)
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if _, err := os.Stat(statePath); err != nil {
t.Fatalf("err: %s", err)
}
state := testStateRead(t, statePath)
if state == nil {
t.Fatal("state should not be nil")
}
}
// test apply with locked state
func TestApply_lockedState(t *testing.T) {
// Create a temporary working directory that is empty