handle unexpected changes to unknown block

An unknown block represents a dynamic configuration block with an
unknown for_each value. We were not catching the case where a provider
modified this value unexpectedly, which would crash with block of type
NestingList blocks where the config value has no length for comparison.
This commit is contained in:
James Bardin 2021-06-11 13:08:26 -04:00
parent 8617d0fed0
commit b7f8ef4dc6
2 changed files with 118 additions and 0 deletions

View File

@ -69,7 +69,20 @@ func assertPlanValid(schema *configschema.Block, priorState, config, plannedStat
// Easy path: nothing has changed at all // Easy path: nothing has changed at all
continue continue
} }
if !configV.IsKnown() {
// An unknown config block represents a dynamic block where the
// for_each value is unknown, and therefor cannot be altered by the
// provider.
errs = append(errs, path.NewErrorf("planned value %#v for unknown dynamic block", plannedV))
continue
}
if !plannedV.IsKnown() { if !plannedV.IsKnown() {
// Only dynamic configuration can set blocks to unknown, so this is
// not allowed from the provider. This means that either the config
// and plan should match, or we have an error where the plan
// changed the config value, both of which have been checked.
errs = append(errs, path.NewErrorf("attribute representing nested block must not be unknown itself; set nested attribute values to unknown instead")) errs = append(errs, path.NewErrorf("attribute representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
continue continue
} }
@ -94,6 +107,7 @@ func assertPlanValid(schema *configschema.Block, priorState, config, plannedStat
errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
continue continue
} }
for it := plannedV.ElementIterator(); it.Next(); { for it := plannedV.ElementIterator(); it.Next(); {
idx, plannedEV := it.Element() idx, plannedEV := it.Element()
path := append(path, cty.IndexStep{Key: idx}) path := append(path, cty.IndexStep{Key: idx})

View File

@ -388,6 +388,110 @@ func TestAssertPlanValid(t *testing.T) {
`.b: attribute representing a list of nested blocks must be empty to indicate no blocks, not null`, `.b: attribute representing a list of nested blocks must be empty to indicate no blocks, not null`,
}, },
}, },
// blocks can be unknown when using dynamic
"nested list, unknown nested dynamic": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"a": {
Nesting: configschema.NestingList,
Block: configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
"computed": {
Type: cty.String,
Computed: true,
},
},
},
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"computed": cty.NullVal(cty.String),
"b": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("x"),
})}),
})}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"b": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
"c": cty.String,
"computed": cty.String,
}))),
})}),
}),
cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"b": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
"c": cty.String,
"computed": cty.String,
}))),
})}),
}),
[]string{},
},
"nested set, unknown dynamic cannot be planned": {
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"computed": {
Type: cty.String,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"b": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"c": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"computed": cty.NullVal(cty.String),
"b": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("x"),
})}),
}),
cty.ObjectVal(map[string]cty.Value{
"computed": cty.NullVal(cty.String),
"b": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
"c": cty.String,
}))),
}),
cty.ObjectVal(map[string]cty.Value{
"computed": cty.StringVal("default"),
"b": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
"c": cty.StringVal("oops"),
})}),
}),
[]string{
`.b: planned value cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"c":cty.StringVal("oops")})}) for unknown dynamic block`,
},
},
"nested set, null in plan": { "nested set, null in plan": {
&configschema.Block{ &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{ BlockTypes: map[string]*configschema.NestedBlock{