Merge pull request #29482 from hashicorp/jbardin/computed-obj-attrs
Computed values within nested object types in `AssertPlanValid`
This commit is contained in:
commit
7c02ef6220
|
@ -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 {
|
func assertPlanValid(schema *configschema.Block, priorState, config, plannedState cty.Value, path cty.Path) []error {
|
||||||
var errs []error
|
var errs []error
|
||||||
if plannedState.IsNull() && !config.IsNull() {
|
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
|
return errs
|
||||||
}
|
}
|
||||||
if config.IsNull() && !plannedState.IsNull() {
|
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
|
return errs
|
||||||
}
|
}
|
||||||
if plannedState.IsNull() {
|
if plannedState.IsNull() {
|
||||||
|
@ -286,6 +286,11 @@ func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, pla
|
||||||
}
|
}
|
||||||
return errs
|
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
|
// 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
|
var errs []error
|
||||||
|
|
||||||
if planned.IsNull() && !config.IsNull() {
|
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
|
return errs
|
||||||
}
|
}
|
||||||
if config.IsNull() && !planned.IsNull() {
|
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
|
return errs
|
||||||
}
|
}
|
||||||
if planned.IsNull() {
|
if planned.IsNull() {
|
||||||
|
@ -349,10 +354,6 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne
|
||||||
for it := planned.ElementIterator(); it.Next(); {
|
for it := planned.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})
|
||||||
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() {
|
if !config.HasIndex(idx).True() {
|
||||||
continue // should never happen since we checked the lengths above
|
continue // should never happen since we checked the lengths above
|
||||||
}
|
}
|
||||||
|
@ -368,94 +369,55 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne
|
||||||
|
|
||||||
case configschema.NestingMap:
|
case configschema.NestingMap:
|
||||||
// A NestingMap might either be a map or an object, depending on
|
// A NestingMap might either be a map or an object, depending on
|
||||||
// whether there are dynamically-typed attributes inside, but
|
// whether there are dynamically-typed attributes inside, so we will
|
||||||
// that's decided statically and so all values will have the same
|
// break these down to maps to handle them both in the same manner.
|
||||||
// kind.
|
plannedVals := map[string]cty.Value{}
|
||||||
if planned.Type().IsObjectType() {
|
configVals := map[string]cty.Value{}
|
||||||
plannedAtys := planned.Type().AttributeTypes()
|
priorVals := map[string]cty.Value{}
|
||||||
configAtys := config.Type().AttributeTypes()
|
|
||||||
for k := range plannedAtys {
|
|
||||||
if _, ok := configAtys[k]; !ok {
|
|
||||||
errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path := append(path, cty.GetAttrStep{Name: k})
|
|
||||||
|
|
||||||
plannedEV := planned.GetAttr(k)
|
if !planned.IsNull() {
|
||||||
if !plannedEV.IsKnown() {
|
plannedVals = planned.AsValueMap()
|
||||||
errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead"))
|
}
|
||||||
continue
|
if !config.IsNull() {
|
||||||
}
|
configVals = config.AsValueMap()
|
||||||
configEV := config.GetAttr(k)
|
}
|
||||||
priorEV := cty.NullVal(schema.ImpliedType())
|
if !prior.IsNull() {
|
||||||
if !prior.IsNull() && prior.Type().HasAttribute(k) {
|
priorVals = prior.AsValueMap()
|
||||||
priorEV = prior.GetAttr(k)
|
}
|
||||||
}
|
|
||||||
moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path)
|
for k, plannedEV := range plannedVals {
|
||||||
errs = append(errs, moreErrs...)
|
configEV, ok := configVals[k]
|
||||||
|
if !ok {
|
||||||
|
errs = append(errs, path.NewErrorf("map key %q from plan is not present in config", k))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
for k := range configAtys {
|
path := append(path, cty.GetAttrStep{Name: k})
|
||||||
if _, ok := plannedAtys[k]; !ok {
|
|
||||||
errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k))
|
priorEV, ok := priorVals[k]
|
||||||
continue
|
if !ok {
|
||||||
}
|
priorEV = cty.NullVal(schema.ImpliedType())
|
||||||
}
|
}
|
||||||
} else {
|
moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path)
|
||||||
plannedL := planned.LengthInt()
|
errs = append(errs, moreErrs...)
|
||||||
configL := config.LengthInt()
|
}
|
||||||
if plannedL != configL {
|
for k := range configVals {
|
||||||
errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
|
if _, ok := plannedVals[k]; !ok {
|
||||||
return errs
|
errs = append(errs, path.NewErrorf("map key %q from config is not present in plan", k))
|
||||||
}
|
continue
|
||||||
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
|
|
||||||
}
|
|
||||||
k := idx.AsString()
|
|
||||||
if !config.HasIndex(idx).True() {
|
|
||||||
errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
configEV := config.Index(idx)
|
|
||||||
priorEV := cty.NullVal(schema.ImpliedType())
|
|
||||||
if !prior.IsNull() && prior.HasIndex(idx).True() {
|
|
||||||
priorEV = prior.Index(idx)
|
|
||||||
}
|
|
||||||
moreErrs := assertPlannedObjectValid(schema, priorEV, configEV, plannedEV, path)
|
|
||||||
errs = append(errs, moreErrs...)
|
|
||||||
}
|
|
||||||
for it := config.ElementIterator(); it.Next(); {
|
|
||||||
idx, _ := it.Element()
|
|
||||||
if !planned.HasIndex(idx).True() {
|
|
||||||
errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", idx.AsString()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case configschema.NestingSet:
|
case configschema.NestingSet:
|
||||||
// Because set elements have no identifier with which to correlate
|
plannedL := planned.LengthInt()
|
||||||
// them, we can't robustly validate the plan for a nested block
|
configL := config.LengthInt()
|
||||||
// backed by a set, and so unfortunately we need to just trust the
|
if plannedL != configL {
|
||||||
// provider to do the right thing. :(
|
errs = append(errs, path.NewErrorf("count in plan (%d) disagrees with count in config (%d)", plannedL, configL))
|
||||||
//
|
return errs
|
||||||
// (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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 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
|
return errs
|
||||||
|
|
|
@ -1222,6 +1222,236 @@ 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`},
|
[]string{`.bloop: planned value cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"blop":cty.StringVal("ok")})}) for a non-computed attribute`},
|
||||||
},
|
},
|
||||||
|
"computed within 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// When an object has dynamic attrs, the map may be
|
||||||
|
// handled as an object.
|
||||||
|
"map_as_obj": {
|
||||||
|
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,
|
||||||
|
})),
|
||||||
|
"map_as_obj": cty.Map(cty.Object(map[string]cty.Type{
|
||||||
|
"name": cty.DynamicPseudoType,
|
||||||
|
})),
|
||||||
|
"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.MapVal(map[string]cty.Value{
|
||||||
|
"one": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"map_as_obj": cty.MapVal(map[string]cty.Value{
|
||||||
|
"one": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.DynamicPseudoType),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"list": cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"set": cty.SetVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"single": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"map": cty.MapVal(map[string]cty.Value{
|
||||||
|
"one": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"map_as_obj": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"one": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.StringVal("computed"),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"list": cty.ListVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"set": cty.SetVal([]cty.Value{
|
||||||
|
cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"single": cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"name": cty.NullVal(cty.String),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
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 {
|
for name, test := range tests {
|
||||||
|
|
Loading…
Reference in New Issue