Skip to content

Commit dc7dc61

Browse files
committed
Type Block need handle attribute marked as removed contionally.
In some provider, e.g. AzureRM, TypeSet is use quite prevalent to hold a set of sub-resources (e.g. security_rule in azurerm_network_security_group). The reason for using TypeSet instead of TypeList might because service return a collection of info with arbitrary order, or other reasons. In this case: Given a slice of nested blocks, if one of the nested block (B1) attributes is optional and has no default value, and user didn't specify a value for it. Then if another nested block (B2) changed, which triggers some diff, then B1 will also be replaced. That is because the optional attribute contribute a diff of "zero value" -> `null`, which changed the hash of the block. This fix is to carefully handle this case. We keep attribute marked as `NewRemoved` after a `diffString` only when all the attributes are marked as such. Otherwise, as long as one attribute is not marked to be removed, that means this block will be kept. Then we will manipulate the attributes in this block, which being marked as removed just because user didn't specify a value and the original value is the zero value. This keeps consistent as other Resource types (e.g. List Resource). (Though those type just remove the attribute from diff set, while for Set we need to return the complete state as we will not depend on the old state during diff apply).
1 parent b025e7d commit dc7dc61

File tree

2 files changed

+204
-4
lines changed

2 files changed

+204
-4
lines changed

helper/schema/schema.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,13 +1305,67 @@ func (m schemaMap) diffSet(
13051305
switch t := schema.Elem.(type) {
13061306
case *Resource:
13071307
// This is a complex resource
1308+
1309+
// As long as one of the attribute is not marked to be removed, we should unmark the other attributes
1310+
// that are marked to be removed just because their new value is not specified in config and their
1311+
// old value is the zero value.
1312+
isAllSubKNewRemoved := true
1313+
13081314
for k2, schema := range t.Schema {
13091315
subK := fmt.Sprintf("%s.%s.%s", k, code, k2)
13101316
err := m.diff(subK, schema, diff, d, true)
13111317
if err != nil {
13121318
return err
13131319
}
1320+
1321+
if subV, ok := diff.Attributes[subK]; ok && !subV.NewRemoved {
1322+
isAllSubKNewRemoved = false
1323+
}
13141324
}
1325+
if !isAllSubKNewRemoved {
1326+
for k2 := range t.Schema {
1327+
subK := fmt.Sprintf("%s.%s.%s", k, code, k2)
1328+
if subV, ok := diff.Attributes[subK]; ok && subV.NewRemoved {
1329+
schemaList := addrToSchema(strings.Split(subK, "."), map[string]*Schema{k: schema})
1330+
if len(schemaList) == 0 {
1331+
continue
1332+
}
1333+
subSchema := schemaList[len(schemaList)-1]
1334+
1335+
var oldIsZero bool
1336+
switch subSchema.Type {
1337+
case TypeBool:
1338+
ov := subV.Old == "true"
1339+
oldIsZero = ov == subSchema.ZeroValue()
1340+
case TypeInt:
1341+
ov, err := strconv.Atoi(subV.Old)
1342+
if err != nil {
1343+
return err
1344+
}
1345+
oldIsZero = ov == subSchema.ZeroValue()
1346+
case TypeFloat:
1347+
ov, err := strconv.ParseFloat(subV.Old, 64)
1348+
if err != nil {
1349+
return err
1350+
}
1351+
oldIsZero = ov == subSchema.ZeroValue()
1352+
case TypeString:
1353+
oldIsZero = subV.Old == subSchema.ZeroValue()
1354+
case TypeList, TypeSet:
1355+
// TODO
1356+
case TypeMap:
1357+
// TODO
1358+
default:
1359+
return fmt.Errorf("%s: unknown type %#v", k, schema.Type)
1360+
}
1361+
if oldIsZero{
1362+
subV.NewRemoved = false
1363+
subV.New = subV.Old
1364+
}
1365+
}
1366+
}
1367+
}
1368+
13151369
case *Schema:
13161370
// Copy the schema so that we can set Computed/ForceNew from
13171371
// the parent schema (the TypeSet).

helper/schema/schema_test.go

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
package schema
22

33
import (
4-
"bytes"
5-
"errors"
64
"fmt"
75
"os"
86
"reflect"
97
"sort"
10-
"strconv"
118
"strings"
129
"testing"
1310

14-
"github.com/hashicorp/terraform-plugin-sdk/helper/hashcode"
1511
"github.com/hashicorp/terraform-plugin-sdk/internal/configs/hcl2shim"
1612
"github.com/hashicorp/terraform-plugin-sdk/terraform"
1713
)
@@ -3161,6 +3157,156 @@ func TestSchemaMap_Diff(t *testing.T) {
31613157
},
31623158
},
31633159
},
3160+
{
3161+
Name: "Set element with unset Optional attributes should not be affected when another element get updated",
3162+
Schema: map[string]*Schema{
3163+
"foo": {
3164+
Type: TypeSet,
3165+
Required: true,
3166+
Elem: &Resource{
3167+
Schema: map[string]*Schema{
3168+
"a": {
3169+
Type: TypeInt,
3170+
Required: true,
3171+
},
3172+
"b": {
3173+
Type: TypeInt,
3174+
Optional: true,
3175+
},
3176+
},
3177+
},
3178+
Set: func(v interface{}) int {
3179+
m := v.(map[string]interface{})
3180+
return m["a"].(int) + m["b"].(int)
3181+
},
3182+
},
3183+
},
3184+
3185+
State: &terraform.InstanceState{
3186+
Attributes: map[string]string{
3187+
"foo.#": "2",
3188+
"foo.1.a": "1",
3189+
"foo.1.b": "0",
3190+
"foo.2.a": "2",
3191+
"foo.2.b": "0",
3192+
},
3193+
},
3194+
3195+
Config: map[string]interface{}{
3196+
"foo": []interface{}{
3197+
map[string]interface{}{
3198+
"a": 1,
3199+
},
3200+
map[string]interface{}{
3201+
"a": 3,
3202+
},
3203+
},
3204+
},
3205+
3206+
Diff: &terraform.InstanceDiff{
3207+
Attributes: map[string]*terraform.ResourceAttrDiff{
3208+
"foo.1.a": {
3209+
Old: "1",
3210+
New: "1",
3211+
},
3212+
"foo.1.b": {
3213+
Old: "0",
3214+
New: "0",
3215+
},
3216+
"foo.2.a": {
3217+
Old: "2",
3218+
New: "0",
3219+
NewRemoved: true,
3220+
},
3221+
"foo.2.b": {
3222+
Old: "0",
3223+
New: "0",
3224+
NewRemoved: true,
3225+
},
3226+
"foo.3.a": {
3227+
Old: "",
3228+
New: "3",
3229+
},
3230+
"foo.3.b": {
3231+
Old: "",
3232+
New: "",
3233+
},
3234+
},
3235+
},
3236+
3237+
Err: false,
3238+
},
3239+
{
3240+
Name: "Set element with unset Optional attributes should not be affected when new element get added",
3241+
Schema: map[string]*Schema{
3242+
"foo": {
3243+
Type: TypeSet,
3244+
Required: true,
3245+
Elem: &Resource{
3246+
Schema: map[string]*Schema{
3247+
"a": {
3248+
Type: TypeInt,
3249+
Required: true,
3250+
},
3251+
"b": {
3252+
Type: TypeInt,
3253+
Optional: true,
3254+
},
3255+
},
3256+
},
3257+
Set: func(v interface{}) int {
3258+
m := v.(map[string]interface{})
3259+
return m["a"].(int) + m["b"].(int)
3260+
},
3261+
},
3262+
},
3263+
3264+
State: &terraform.InstanceState{
3265+
Attributes: map[string]string{
3266+
"foo.#": "1",
3267+
"foo.1.a": "1",
3268+
"foo.1.b": "0",
3269+
},
3270+
},
3271+
3272+
Config: map[string]interface{}{
3273+
"foo": []interface{}{
3274+
map[string]interface{}{
3275+
"a": 1,
3276+
},
3277+
map[string]interface{}{
3278+
"a": 2,
3279+
},
3280+
},
3281+
},
3282+
3283+
Diff: &terraform.InstanceDiff{
3284+
Attributes: map[string]*terraform.ResourceAttrDiff{
3285+
"foo.#": {
3286+
Old: "1",
3287+
New: "2",
3288+
},
3289+
"foo.1.a": {
3290+
Old: "1",
3291+
New: "1",
3292+
},
3293+
"foo.1.b": {
3294+
Old: "0",
3295+
New: "0",
3296+
},
3297+
"foo.2.a": {
3298+
Old: "",
3299+
New: "2",
3300+
},
3301+
"foo.2.b": {
3302+
Old: "",
3303+
New: "",
3304+
},
3305+
},
3306+
},
3307+
3308+
Err: false,
3309+
},
31643310
}
31653311

31663312
for i, tc := range cases {

0 commit comments

Comments
 (0)