command/format: Diffs for NestingMap block types
Our initial prototype of new-style diff rendering excluded this because the old SDK has no support for this construct. However, we want to be able to introduce this construct in the new SDK without breaking compatibility with existing versions of Terraform Core, so we need to implement it now so it's ready to be used once the SDK implements it. The key associated with each block allows us to properly correlate the items to recognize the difference between an in-place update of an existing block and the addition/deletion of a block.
This commit is contained in:
parent
dd1fa322a7
commit
2d41c1009b
|
@ -400,8 +400,56 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config
|
|||
}
|
||||
|
||||
case configschema.NestingMap:
|
||||
// TODO: Implement this, once helper/schema is actually able to
|
||||
// produce schemas containing nested map block types.
|
||||
// For the sake of handling nested blocks, we'll treat a null map
|
||||
// the same as an empty map since the config language doesn't
|
||||
// distinguish these anyway.
|
||||
old = ctyNullBlockMapAsEmpty(old)
|
||||
new = ctyNullBlockMapAsEmpty(new)
|
||||
|
||||
oldItems := old.AsValueMap()
|
||||
newItems := new.AsValueMap()
|
||||
if (len(oldItems) + len(newItems)) == 0 {
|
||||
// Nothing to do if both maps are empty
|
||||
return
|
||||
}
|
||||
|
||||
allKeys := make(map[string]bool)
|
||||
for k := range oldItems {
|
||||
allKeys[k] = true
|
||||
}
|
||||
for k := range newItems {
|
||||
allKeys[k] = true
|
||||
}
|
||||
allKeysOrder := make([]string, 0, len(allKeys))
|
||||
for k := range allKeys {
|
||||
allKeysOrder = append(allKeysOrder, k)
|
||||
}
|
||||
sort.Strings(allKeysOrder)
|
||||
|
||||
if blankBefore {
|
||||
p.buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
for _, k := range allKeysOrder {
|
||||
var action plans.Action
|
||||
oldValue := oldItems[k]
|
||||
newValue := newItems[k]
|
||||
switch {
|
||||
case oldValue == cty.NilVal:
|
||||
oldValue = cty.NullVal(newValue.Type())
|
||||
action = plans.Create
|
||||
case newValue == cty.NilVal:
|
||||
newValue = cty.NullVal(oldValue.Type())
|
||||
action = plans.Delete
|
||||
case !newValue.RawEquals(oldValue):
|
||||
action = plans.Update
|
||||
default:
|
||||
action = plans.NoOp
|
||||
}
|
||||
|
||||
path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
|
||||
p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2586,6 +2586,321 @@ func TestResourceChange_nestedSet(t *testing.T) {
|
|||
runTestCases(t, testCases)
|
||||
}
|
||||
|
||||
func TestResourceChange_nestedMap(t *testing.T) {
|
||||
testCases := map[string]testCase{
|
||||
"in-place update - creation": {
|
||||
Action: plans.Update,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-BEFORE"),
|
||||
"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
|
||||
"volume_type": cty.String,
|
||||
})),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-AFTER"),
|
||||
"root_block_device": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("gp2"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
Tainted: false,
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"root_block_device": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"volume_type": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingMap,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||
~ resource "test_instance" "example" {
|
||||
~ ami = "ami-BEFORE" -> "ami-AFTER"
|
||||
id = "i-02ae66f368e8518a9"
|
||||
|
||||
+ root_block_device "a" {
|
||||
+ volume_type = "gp2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
"in-place update - change attr": {
|
||||
Action: plans.Update,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-BEFORE"),
|
||||
"root_block_device": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("gp2"),
|
||||
"new_field": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-AFTER"),
|
||||
"root_block_device": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("gp2"),
|
||||
"new_field": cty.StringVal("new_value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
Tainted: false,
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"root_block_device": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"volume_type": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"new_field": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingMap,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||
~ resource "test_instance" "example" {
|
||||
~ ami = "ami-BEFORE" -> "ami-AFTER"
|
||||
id = "i-02ae66f368e8518a9"
|
||||
|
||||
~ root_block_device "a" {
|
||||
+ new_field = "new_value"
|
||||
volume_type = "gp2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
"in-place update - insertion": {
|
||||
Action: plans.Update,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-BEFORE"),
|
||||
"root_block_device": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("gp2"),
|
||||
"new_field": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-AFTER"),
|
||||
"root_block_device": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("gp2"),
|
||||
"new_field": cty.NullVal(cty.String),
|
||||
}),
|
||||
"b": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("gp2"),
|
||||
"new_field": cty.StringVal("new_value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
Tainted: false,
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"root_block_device": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"volume_type": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"new_field": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingMap,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||
~ resource "test_instance" "example" {
|
||||
~ ami = "ami-BEFORE" -> "ami-AFTER"
|
||||
id = "i-02ae66f368e8518a9"
|
||||
|
||||
root_block_device "a" {
|
||||
volume_type = "gp2"
|
||||
}
|
||||
+ root_block_device "b" {
|
||||
+ new_field = "new_value"
|
||||
+ volume_type = "gp2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
"force-new update (whole block)": {
|
||||
Action: plans.DeleteThenCreate,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-BEFORE"),
|
||||
"root_block_device": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("gp2"),
|
||||
}),
|
||||
"b": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("standard"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-AFTER"),
|
||||
"root_block_device": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("different"),
|
||||
}),
|
||||
"b": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("standard"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
RequiredReplace: cty.NewPathSet(cty.Path{
|
||||
cty.GetAttrStep{Name: "root_block_device"},
|
||||
cty.IndexStep{Key: cty.StringVal("a")},
|
||||
}),
|
||||
Tainted: false,
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"root_block_device": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"volume_type": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingMap,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: ` # test_instance.example must be replaced
|
||||
-/+ resource "test_instance" "example" {
|
||||
~ ami = "ami-BEFORE" -> "ami-AFTER"
|
||||
id = "i-02ae66f368e8518a9"
|
||||
|
||||
~ root_block_device "a" { # forces replacement
|
||||
~ volume_type = "gp2" -> "different"
|
||||
}
|
||||
root_block_device "b" {
|
||||
volume_type = "standard"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
"in-place update - deletion": {
|
||||
Action: plans.Update,
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Before: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-BEFORE"),
|
||||
"root_block_device": cty.MapVal(map[string]cty.Value{
|
||||
"a": cty.ObjectVal(map[string]cty.Value{
|
||||
"volume_type": cty.StringVal("gp2"),
|
||||
"new_field": cty.StringVal("new_value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-02ae66f368e8518a9"),
|
||||
"ami": cty.StringVal("ami-AFTER"),
|
||||
"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
|
||||
"volume_type": cty.String,
|
||||
"new_field": cty.String,
|
||||
})),
|
||||
}),
|
||||
RequiredReplace: cty.NewPathSet(),
|
||||
Tainted: false,
|
||||
Schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"root_block_device": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"volume_type": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"new_field": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingMap,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedOutput: ` # test_instance.example will be updated in-place
|
||||
~ resource "test_instance" "example" {
|
||||
~ ami = "ami-BEFORE" -> "ami-AFTER"
|
||||
id = "i-02ae66f368e8518a9"
|
||||
|
||||
- root_block_device "a" {
|
||||
- new_field = "new_value" -> null
|
||||
- volume_type = "gp2" -> null
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
runTestCases(t, testCases)
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
Action plans.Action
|
||||
Mode addrs.ResourceMode
|
||||
|
@ -2645,7 +2960,7 @@ func runTestCases(t *testing.T, testCases map[string]testCase) {
|
|||
|
||||
output := ResourceChange(change, tc.Tainted, tc.Schema, color)
|
||||
if output != tc.ExpectedOutput {
|
||||
t.Fatalf("Unexpected diff.\nExpected:\n%s\nGiven:\n%s\n", tc.ExpectedOutput, output)
|
||||
t.Fatalf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.ExpectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue