fix panics when handling null values in maps

NestingMap structures are not well tested, and we panic in many
situations when null crops up. Fix the first test cases and start
refactoring best we can. This probably won't go so far as making all the
objchange functions generic over Block and Object, but we can simplify a
lot and verify parity in implementations for now.
This commit is contained in:
James Bardin 2023-01-20 11:34:40 -05:00
parent 8e917e5513
commit fcbfc365e6
2 changed files with 167 additions and 48 deletions

View File

@ -275,6 +275,14 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
return newV return newV
} }
func proposedNewObjectAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) cty.Value {
if config.IsNull() {
return config
}
return cty.ObjectVal(proposedNewAttributes(attrs, prior, config))
}
func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value { func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value {
newAttrs := make(map[string]cty.Value, len(attrs)) newAttrs := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs { for name, attr := range attrs {
@ -324,7 +332,7 @@ func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, conf
func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value { func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value {
// if the config isn't known at all, then we must use that value // if the config isn't known at all, then we must use that value
if !config.IsNull() && !config.IsKnown() { if !config.IsKnown() {
return config return config
} }
@ -340,7 +348,7 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
break break
} }
newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config)) newV = proposedNewObjectAttributes(schema.Attributes, prior, config)
case configschema.NestingList: case configschema.NestingList:
// Nested blocks are correlated by index. // Nested blocks are correlated by index.
@ -361,8 +369,8 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
} }
priorEV := prior.Index(idx) priorEV := prior.Index(idx)
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) newEV := proposedNewObjectAttributes(schema.Attributes, priorEV, configEV)
newVals = append(newVals, cty.ObjectVal(newEV)) newVals = append(newVals, newEV)
} }
// Despite the name, a NestingList might also be a tuple, if // Despite the name, a NestingList might also be a tuple, if
// its nested schema contains dynamically-typed attributes. // its nested schema contains dynamically-typed attributes.
@ -374,17 +382,22 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
} }
case configschema.NestingMap: case configschema.NestingMap:
configVLen := 0
if config.IsKnown() && !config.IsNull() {
configVLen = config.LengthInt()
}
if configVLen == 0 {
break
}
newVals := make(map[string]cty.Value, configVLen)
// Despite the name, a NestingMap may produce either a map or // Despite the name, a NestingMap may produce either a map or
// object value, depending on whether the nested schema contains // object value, depending on whether the nested schema contains
// dynamically-typed attributes. // dynamically-typed attributes.
if config.Type().IsObjectType() { if config.Type().IsObjectType() {
// Nested blocks are correlated by key. // Nested blocks are correlated by key.
configVLen := 0
if config.IsKnown() && !config.IsNull() {
configVLen = config.LengthInt()
}
if configVLen > 0 {
newVals := make(map[string]cty.Value, configVLen)
atys := config.Type().AttributeTypes() atys := config.Type().AttributeTypes()
for name := range atys { for name := range atys {
configEV := config.GetAttr(name) configEV := config.GetAttr(name)
@ -395,21 +408,14 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
continue continue
} }
priorEV := prior.GetAttr(name) priorEV := prior.GetAttr(name)
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) newEV := proposedNewObjectAttributes(schema.Attributes, priorEV, configEV)
newVals[name] = cty.ObjectVal(newEV) newVals[name] = newEV
} }
// Although we call the nesting mode "map", we actually use // Although we call the nesting mode "map", we actually use
// object values so that elements might have different types // object values so that elements might have different types
// in case of dynamically-typed attributes. // in case of dynamically-typed attributes.
newV = cty.ObjectVal(newVals) newV = cty.ObjectVal(newVals)
}
} else { } else {
configVLen := 0
if config.IsKnown() && !config.IsNull() {
configVLen = config.LengthInt()
}
if configVLen > 0 {
newVals := make(map[string]cty.Value, configVLen)
for it := config.ElementIterator(); it.Next(); { for it := config.ElementIterator(); it.Next(); {
idx, configEV := it.Element() idx, configEV := it.Element()
k := idx.AsString() k := idx.AsString()
@ -421,12 +427,11 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
} }
priorEV := prior.Index(idx) priorEV := prior.Index(idx)
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) newEV := proposedNewObjectAttributes(schema.Attributes, priorEV, configEV)
newVals[k] = cty.ObjectVal(newEV) newVals[k] = newEV
} }
newV = cty.MapVal(newVals) newV = cty.MapVal(newVals)
} }
}
case configschema.NestingSet: case configschema.NestingSet:
// Nested blocks are correlated by comparing the element values // Nested blocks are correlated by comparing the element values
@ -461,8 +466,8 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
if priorEV == cty.NilVal { if priorEV == cty.NilVal {
newVals = append(newVals, configEV) newVals = append(newVals, configEV)
} else { } else {
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) newEV := proposedNewObjectAttributes(schema.Attributes, priorEV, configEV)
newVals = append(newVals, cty.ObjectVal(newEV)) newVals = append(newVals, newEV)
} }
} }
newV = cty.SetVal(newVals) newV = cty.SetVal(newVals)

View File

@ -752,6 +752,120 @@ func TestProposedNew(t *testing.T) {
}), }),
}), }),
}, },
"prior optional computed nested map elem to null": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingMap,
Attributes: map[string]*configschema.Attribute{
"blop": {
Type: cty.String,
Optional: true,
},
"bleep": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.MapVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("glub"),
"bleep": cty.StringVal("computed"),
}),
"b": cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("blub"),
"bleep": cty.StringVal("computed"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.MapVal(map[string]cty.Value{
"a": cty.NullVal(cty.Object(map[string]cty.Type{
"blop": cty.String,
"bleep": cty.String,
})),
"c": cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("blub"),
"bleep": cty.NullVal(cty.String),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.MapVal(map[string]cty.Value{
"a": cty.NullVal(cty.Object(map[string]cty.Type{
"blop": cty.String,
"bleep": cty.String,
})),
"c": cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("blub"),
"bleep": cty.NullVal(cty.String),
}),
}),
}),
},
"prior optional computed nested map to null": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingMap,
Attributes: map[string]*configschema.Attribute{
"blop": {
Type: cty.String,
Optional: true,
},
"bleep": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},
Optional: true,
Computed: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.MapVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("glub"),
"bleep": cty.StringVal("computed"),
}),
"b": cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("blub"),
"bleep": cty.StringVal("computed"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.Map(
cty.Object(map[string]cty.Type{
"blop": cty.String,
"bleep": cty.String,
}),
)),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.NullVal(cty.Map(
cty.Object(map[string]cty.Type{
"blop": cty.String,
"bleep": cty.String,
}),
)),
}),
},
"prior nested map with dynamic": { "prior nested map with dynamic": {
&configschema.Block{ &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{ BlockTypes: map[string]*configschema.NestedBlock{