Merge pull request from hashicorp/jbardin/nesting-map-objchange

Fix crashes with NestingMap values
This commit is contained in:
James Bardin 2023-01-25 15:05:27 -05:00 committed by GitHub
commit 33c1ffa7c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 198 additions and 112 deletions
internal/plans/objchange

View File

@ -107,7 +107,7 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
return config
}
var newV cty.Value
newV := config
switch schema.Nesting {
case configschema.NestingSingle:
@ -162,63 +162,43 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
}
case configschema.NestingMap:
// Despite the name, a NestingMap may produce either a map or
// object value, depending on whether the nested schema contains
// dynamically-typed attributes.
if config.Type().IsObjectType() {
// 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()
for name := range atys {
configEV := config.GetAttr(name)
if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
// If there is no corresponding prior element then
// we just take the config value as-is.
newVals[name] = configEV
continue
}
priorEV := prior.GetAttr(name)
newVals := map[string]cty.Value{}
newEV := ProposedNew(&schema.Block, priorEV, configEV)
newVals[name] = newEV
}
// Although we call the nesting mode "map", we actually use
// object values so that elements might have different types
// in case of dynamically-typed attributes.
newV = cty.ObjectVal(newVals)
} else {
newV = cty.EmptyObjectVal
}
} 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(); {
idx, configEV := it.Element()
k := idx.AsString()
if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
// If there is no corresponding prior element then
// we just take the config value as-is.
newVals[k] = configEV
continue
}
priorEV := prior.Index(idx)
if config.IsNull() || !config.IsKnown() || config.LengthInt() == 0 {
// We already assigned newVal and there's nothing to compare in
// config.
break
}
cfgMap := config.AsValueMap()
newEV := ProposedNew(&schema.Block, priorEV, configEV)
newVals[k] = newEV
}
newV = cty.MapVal(newVals)
} else {
newV = cty.MapValEmpty(schema.ImpliedType())
// prior may be null or empty
priorMap := map[string]cty.Value{}
if !prior.IsNull() && prior.IsKnown() && prior.LengthInt() > 0 {
priorMap = prior.AsValueMap()
}
for name, configEV := range cfgMap {
priorEV, inPrior := priorMap[name]
if !inPrior {
// If there is no corresponding prior element then
// we just take the config value as-is.
newVals[name] = configEV
continue
}
newEV := ProposedNew(&schema.Block, priorEV, configEV)
newVals[name] = newEV
}
// The value must leave as the same type it came in as
switch {
case config.Type().IsObjectType():
// Although we call the nesting mode "map", we actually use
// object values so that elements might have different types
// in case of dynamically-typed attributes.
newV = cty.ObjectVal(newVals)
default:
newV = cty.MapVal(newVals)
}
case configschema.NestingSet:
@ -275,6 +255,14 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
return newV
}
func proposedNewObjectAttributes(schema *configschema.Object, prior, config cty.Value) cty.Value {
if config.IsNull() {
return config
}
return cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
}
func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value {
newAttrs := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs {
@ -324,7 +312,7 @@ func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, conf
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 !config.IsNull() && !config.IsKnown() {
if !config.IsKnown() {
return config
}
@ -340,7 +328,7 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
break
}
newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
newV = proposedNewObjectAttributes(schema, prior, config)
case configschema.NestingList:
// Nested blocks are correlated by index.
@ -348,7 +336,6 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
if !config.IsNull() {
configVLen = config.LengthInt()
}
if configVLen > 0 {
newVals := make([]cty.Value, 0, configVLen)
for it := config.ElementIterator(); it.Next(); {
@ -361,8 +348,8 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
}
priorEV := prior.Index(idx)
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
newVals = append(newVals, cty.ObjectVal(newEV))
newEV := proposedNewObjectAttributes(schema, priorEV, configEV)
newVals = append(newVals, newEV)
}
// Despite the name, a NestingList might also be a tuple, if
// its nested schema contains dynamically-typed attributes.
@ -374,58 +361,43 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
}
case configschema.NestingMap:
// Despite the name, a NestingMap may produce either a map or
// object value, depending on whether the nested schema contains
// dynamically-typed attributes.
if config.Type().IsObjectType() {
// 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()
for name := range atys {
configEV := config.GetAttr(name)
if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
// If there is no corresponding prior element then
// we just take the config value as-is.
newVals[name] = configEV
continue
}
priorEV := prior.GetAttr(name)
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
newVals[name] = cty.ObjectVal(newEV)
}
// Although we call the nesting mode "map", we actually use
// object values so that elements might have different types
// in case of dynamically-typed attributes.
newV = cty.ObjectVal(newVals)
}
} 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(); {
idx, configEV := it.Element()
k := idx.AsString()
if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
// If there is no corresponding prior element then
// we just take the config value as-is.
newVals[k] = configEV
continue
}
priorEV := prior.Index(idx)
newVals := map[string]cty.Value{}
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
newVals[k] = cty.ObjectVal(newEV)
}
newV = cty.MapVal(newVals)
if config.IsNull() || !config.IsKnown() || config.LengthInt() == 0 {
// We already assigned newVal and there's nothing to compare in
// config.
break
}
cfgMap := config.AsValueMap()
// prior may be null or empty
priorMap := map[string]cty.Value{}
if !prior.IsNull() && prior.IsKnown() && prior.LengthInt() > 0 {
priorMap = prior.AsValueMap()
}
for name, configEV := range cfgMap {
priorEV, inPrior := priorMap[name]
if !inPrior {
// If there is no corresponding prior element then
// we just take the config value as-is.
newVals[name] = configEV
continue
}
newEV := proposedNewObjectAttributes(schema, priorEV, configEV)
newVals[name] = newEV
}
// The value must leave as the same type it came in as
switch {
case config.Type().IsObjectType():
// Although we call the nesting mode "map", we actually use
// object values so that elements might have different types
// in case of dynamically-typed attributes.
newV = cty.ObjectVal(newVals)
default:
newV = cty.MapVal(newVals)
}
case configschema.NestingSet:
@ -461,8 +433,8 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
if priorEV == cty.NilVal {
newVals = append(newVals, configEV)
} else {
newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
newVals = append(newVals, cty.ObjectVal(newEV))
newEV := proposedNewObjectAttributes(schema, priorEV, configEV)
newVals = append(newVals, newEV)
}
}
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": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{