mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #32553 from hashicorp/jbardin/nesting-map-objchange
Fix crashes with NestingMap values
This commit is contained in:
commit
33c1ffa7c2
internal/plans/objchange
@ -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)
|
||||
|
@ -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{
|
||||
|
Loading…
Reference in New Issue
Block a user