diff --git a/internal/plans/objchange/plan_valid.go b/internal/plans/objchange/plan_valid.go index 5bcfe8dd5..2706a6c52 100644 --- a/internal/plans/objchange/plan_valid.go +++ b/internal/plans/objchange/plan_valid.go @@ -39,11 +39,11 @@ func AssertPlanValid(schema *configschema.Block, priorState, config, plannedStat func assertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value, path cty.Path) []error { var errs []error if plannedState.IsNull() && !config.IsNull() { - errs = append(errs, path.NewErrorf("planned for absense but config wants existence")) + errs = append(errs, path.NewErrorf("planned for absence but config wants existence")) return errs } if config.IsNull() && !plannedState.IsNull() { - errs = append(errs, path.NewErrorf("planned for existence but config wants absense")) + errs = append(errs, path.NewErrorf("planned for existence but config wants absence")) return errs } if plannedState.IsNull() { @@ -286,6 +286,11 @@ func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, pla } return errs } + } else { + if attrS.Computed { + errs = append(errs, path.NewErrorf("configuration present for computed attribute")) + return errs + } } // If this attribute has a NestedType, validate the nested object @@ -317,11 +322,11 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne var errs []error if planned.IsNull() && !config.IsNull() { - errs = append(errs, path.NewErrorf("planned for absense but config wants existence")) + errs = append(errs, path.NewErrorf("planned for absence but config wants existence")) return errs } if config.IsNull() && !planned.IsNull() { - errs = append(errs, path.NewErrorf("planned for existence but config wants absense")) + errs = append(errs, path.NewErrorf("planned for existence but config wants absence")) return errs } if planned.IsNull() { @@ -349,10 +354,6 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne for it := planned.ElementIterator(); it.Next(); { idx, plannedEV := it.Element() path := append(path, cty.IndexStep{Key: idx}) - if !plannedEV.IsKnown() { - errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) - continue - } if !config.HasIndex(idx).True() { continue // should never happen since we checked the lengths above } @@ -387,16 +388,11 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne for k, plannedEV := range plannedVals { configEV, ok := configVals[k] if !ok { - errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k)) + errs = append(errs, path.NewErrorf("map key %q from plan is not present in config", k)) continue } path := append(path, cty.GetAttrStep{Name: k}) - if !plannedEV.IsKnown() { - errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) - continue - } - priorEV, ok := priorVals[k] if !ok { priorEV = cty.NullVal(schema.ImpliedType()) @@ -406,31 +402,22 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne } for k := range configVals { if _, ok := plannedVals[k]; !ok { - errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k)) + errs = append(errs, path.NewErrorf("map key %q from config is not present in plan", k)) continue } } case configschema.NestingSet: - // Because set elements have no identifier with which to correlate - // them, we can't robustly validate the plan for a nested block - // backed by a set, and so unfortunately we need to just trust the - // provider to do the right thing. :( - // - // (In principle we could correlate elements by matching the - // subset of attributes explicitly set in config, except for the - // special diff suppression rule which allows for there to be a - // planned value that is constructed by mixing part of a prior - // value with part of a config value, creating an entirely new - // element that is not present in either prior nor config.) - for it := planned.ElementIterator(); it.Next(); { - idx, plannedEV := it.Element() - path := append(path, cty.IndexStep{Key: idx}) - if !plannedEV.IsKnown() { - errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) - continue - } + plannedL := planned.LengthInt() + configL := config.LengthInt() + if plannedL != configL { + errs = append(errs, path.NewErrorf("count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) + return errs } + // Because set elements have no identifier with which to correlate + // them, we can't robustly validate the plan for a nested object + // backed by a set, and so unfortunately we need to just trust the + // provider to do the right thing. } return errs diff --git a/internal/plans/objchange/plan_valid_test.go b/internal/plans/objchange/plan_valid_test.go index 28150002b..dedb3958b 100644 --- a/internal/plans/objchange/plan_valid_test.go +++ b/internal/plans/objchange/plan_valid_test.go @@ -1222,7 +1222,7 @@ func TestAssertPlanValid(t *testing.T) { }), []string{`.bloop: planned value cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"blop":cty.StringVal("ok")})}) for a non-computed attribute`}, }, - "computed in nested objects": { + "computed within nested objects": { &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "map": { @@ -1353,6 +1353,105 @@ func TestAssertPlanValid(t *testing.T) { }), nil, }, + "computed nested objects": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "map": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + }, + }, + }, + Computed: true, + }, + "list": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + }, + }, + }, + Computed: true, + }, + "set": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + }, + }, + }, + Computed: true, + }, + "single": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.DynamicPseudoType, + }, + }, + }, + Computed: true, + }, + }, + }, + cty.NullVal(cty.Object(map[string]cty.Type{ + "map": cty.Map(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + "list": cty.List(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + "set": cty.Set(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + "single": cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + })), + cty.ObjectVal(map[string]cty.Value{ + "map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ + "name": cty.String, + }))), + "list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "name": cty.String, + }))), + "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "name": cty.String, + }))), + "single": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + }), + cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "one": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + }), + "list": cty.ListVal([]cty.Value{ + cty.UnknownVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + }), + "set": cty.SetVal([]cty.Value{ + cty.UnknownVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + }), + "single": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + }), + nil, + }, } for name, test := range tests {