diff --git a/internal/plans/objchange/plan_valid.go b/internal/plans/objchange/plan_valid.go index 0697bac61..3555e02ed 100644 --- a/internal/plans/objchange/plan_valid.go +++ b/internal/plans/objchange/plan_valid.go @@ -69,7 +69,20 @@ func assertPlanValid(schema *configschema.Block, priorState, config, plannedStat // Easy path: nothing has changed at all 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() { + // 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")) 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)) continue } + for it := plannedV.ElementIterator(); it.Next(); { idx, plannedEV := it.Element() path := append(path, cty.IndexStep{Key: idx}) diff --git a/internal/plans/objchange/plan_valid_test.go b/internal/plans/objchange/plan_valid_test.go index a07ab4c16..a7166fe7b 100644 --- a/internal/plans/objchange/plan_valid_test.go +++ b/internal/plans/objchange/plan_valid_test.go @@ -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`, }, }, + + // 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": { &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{