diff --git a/internal/command/format/diff.go b/internal/command/format/diff.go index c70918af4a..ec777b5852 100644 --- a/internal/command/format/diff.go +++ b/internal/command/format/diff.go @@ -98,6 +98,31 @@ func ResourceChange( default: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] delete (unknown reason %s)"), dispAddr, language)) } + // We can sometimes give some additional detail about why we're + // proposing to delete. We show this as additional notes, rather than + // as additional wording in the main action statement, in an attempt + // to make the "will be destroyed" message prominent and consistent + // in all cases, for easier scanning of this often-risky action. + switch change.ActionReason { + case plans.ResourceInstanceDeleteBecauseNoResourceConfig: + buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", addr.Resource.Resource)) + case plans.ResourceInstanceDeleteBecauseNoModule: + buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", addr.Module)) + case plans.ResourceInstanceDeleteBecauseWrongRepetition: + // We have some different variations of this one + switch addr.Resource.Key.(type) { + case nil: + buf.WriteString("\n # (because resource uses count or for_each)") + case addrs.IntKey: + buf.WriteString("\n # (because resource does not use count)") + case addrs.StringKey: + buf.WriteString("\n # (because resource does not use for_each)") + } + case plans.ResourceInstanceDeleteBecauseCountIndex: + buf.WriteString(fmt.Sprintf("\n # (because index %s is out of range for count)", addr.Resource.Key)) + case plans.ResourceInstanceDeleteBecauseEachKey: + buf.WriteString(fmt.Sprintf("\n # (because key %s is not in for_each map)", addr.Resource.Key)) + } if change.DeposedKey != states.NotDeposed { // Some extra context about this unusual situation. buf.WriteString(color.Color("\n # (left over from a partially-failed replacement of this instance)")) @@ -115,7 +140,7 @@ func ResourceChange( buf.WriteString(color.Color("[reset]\n")) if change.Moved() && change.Action != plans.NoOp { - buf.WriteString(fmt.Sprintf(color.Color("[bold] # [reset]([bold]%s[reset] has moved to [bold]%s[reset])\n"), change.PrevRunAddr.String(), dispAddr)) + buf.WriteString(fmt.Sprintf(color.Color(" # [reset](moved from %s)\n"), change.PrevRunAddr.String())) } if change.Moved() && change.Action == plans.NoOp { diff --git a/internal/command/format/diff_test.go b/internal/command/format/diff_test.go index b59a3af564..6a720210ed 100644 --- a/internal/command/format/diff_test.go +++ b/internal/command/format/diff_test.go @@ -3504,6 +3504,229 @@ func TestResourceChange_nestedMap(t *testing.T) { runTestCases(t, testCases) } +func TestResourceChange_actionReason(t *testing.T) { + emptySchema := &configschema.Block{} + nullVal := cty.NullVal(cty.EmptyObject) + emptyVal := cty.EmptyObjectVal + + testCases := map[string]testCase{ + "delete for no particular reason": { + Action: plans.Delete, + ActionReason: plans.ResourceInstanceChangeNoReason, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example will be destroyed + - resource "test_instance" "example" {} +`, + }, + "delete because of wrong repetition mode (NoKey)": { + Action: plans.Delete, + ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, + Mode: addrs.ManagedResourceMode, + InstanceKey: addrs.NoKey, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example will be destroyed + # (because resource uses count or for_each) + - resource "test_instance" "example" {} +`, + }, + "delete because of wrong repetition mode (IntKey)": { + Action: plans.Delete, + ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, + Mode: addrs.ManagedResourceMode, + InstanceKey: addrs.IntKey(1), + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example[1] will be destroyed + # (because resource does not use count) + - resource "test_instance" "example" {} +`, + }, + "delete because of wrong repetition mode (StringKey)": { + Action: plans.Delete, + ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, + Mode: addrs.ManagedResourceMode, + InstanceKey: addrs.StringKey("a"), + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example["a"] will be destroyed + # (because resource does not use for_each) + - resource "test_instance" "example" {} +`, + }, + "delete because no resource configuration": { + Action: plans.Delete, + ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig, + ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.NoKey), + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # module.foo.test_instance.example will be destroyed + # (because test_instance.example is not in configuration) + - resource "test_instance" "example" {} +`, + }, + "delete because no module": { + Action: plans.Delete, + ActionReason: plans.ResourceInstanceDeleteBecauseNoModule, + ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)), + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # module.foo[1].test_instance.example will be destroyed + # (because module.foo[1] is not in configuration) + - resource "test_instance" "example" {} +`, + }, + "delete because out of range for count": { + Action: plans.Delete, + ActionReason: plans.ResourceInstanceDeleteBecauseCountIndex, + Mode: addrs.ManagedResourceMode, + InstanceKey: addrs.IntKey(1), + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example[1] will be destroyed + # (because index [1] is out of range for count) + - resource "test_instance" "example" {} +`, + }, + "delete because out of range for for_each": { + Action: plans.Delete, + ActionReason: plans.ResourceInstanceDeleteBecauseEachKey, + Mode: addrs.ManagedResourceMode, + InstanceKey: addrs.StringKey("boop"), + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example["boop"] will be destroyed + # (because key ["boop"] is not in for_each map) + - resource "test_instance" "example" {} +`, + }, + "replace for no particular reason (delete first)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceChangeNoReason, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" {} +`, + }, + "replace for no particular reason (create first)": { + Action: plans.CreateThenDelete, + ActionReason: plans.ResourceInstanceChangeNoReason, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example must be replaced ++/- resource "test_instance" "example" {} +`, + }, + "replace by request (delete first)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceByRequest, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example will be replaced, as requested +-/+ resource "test_instance" "example" {} +`, + }, + "replace by request (create first)": { + Action: plans.CreateThenDelete, + ActionReason: plans.ResourceInstanceReplaceByRequest, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example will be replaced, as requested ++/- resource "test_instance" "example" {} +`, + }, + "replace because tainted (delete first)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceBecauseTainted, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example is tainted, so must be replaced +-/+ resource "test_instance" "example" {} +`, + }, + "replace because tainted (create first)": { + Action: plans.CreateThenDelete, + ActionReason: plans.ResourceInstanceReplaceBecauseTainted, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example is tainted, so must be replaced ++/- resource "test_instance" "example" {} +`, + }, + "replace because cannot update (delete first)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + // This one has no special message, because the fuller explanation + // typically appears inline as a "# forces replacement" comment. + // (not shown here) + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" {} +`, + }, + "replace because cannot update (create first)": { + Action: plans.CreateThenDelete, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + Mode: addrs.ManagedResourceMode, + Before: emptyVal, + After: nullVal, + Schema: emptySchema, + RequiredReplace: cty.NewPathSet(), + // This one has no special message, because the fuller explanation + // typically appears inline as a "# forces replacement" comment. + // (not shown here) + ExpectedOutput: ` # test_instance.example must be replaced ++/- resource "test_instance" "example" {} +`, + }, + } + + runTestCases(t, testCases) +} + func TestResourceChange_sensitiveVariable(t *testing.T) { testCases := map[string]testCase{ "creation": { @@ -4479,7 +4702,7 @@ func TestResourceChange_moved(t *testing.T) { }, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be updated in-place - # (test_instance.previous has moved to test_instance.example) + # (moved from test_instance.previous) ~ resource "test_instance" "example" { ~ bar = "baz" -> "boop" id = "12345" @@ -4524,7 +4747,9 @@ func TestResourceChange_moved(t *testing.T) { type testCase struct { Action plans.Action ActionReason plans.ResourceInstanceChangeActionReason + ModuleInst addrs.ModuleInstance Mode addrs.ResourceMode + InstanceKey addrs.InstanceKey DeposedKey states.DeposedKey Before cty.Value BeforeValMarks []cty.PathValueMarks @@ -4571,7 +4796,7 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { Mode: tc.Mode, Type: "test_instance", Name: "example", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + }.Instance(tc.InstanceKey).Absolute(tc.ModuleInst) prevRunAddr := tc.PrevRunAddr // If no previous run address is given, reuse the current address