Skip to content

Commit

Permalink
better determine when to plan optional+computed
Browse files Browse the repository at this point in the history
We can check if an object in state must have at least partially come
from configuration, by seeing if the prior value has any non-null
attributes which are not computed in the schema.

This is used when the configuration contains a null optional+computed
value, and we want to know if we should plan to send the null value or
the prior state.

Simplify the proposedNewAttributes cases, and add another test for
coverage.
  • Loading branch information
jbardin committed Jan 20, 2023
1 parent e16b848 commit 0462b84
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 58 deletions.
149 changes: 101 additions & 48 deletions internal/plans/objchange/objchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
return newV
}

func proposedNewObjectAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) cty.Value {
if config.IsNull() {
return config
}

return cty.ObjectVal(proposedNewAttributes(attrs, 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 {
Expand All @@ -297,6 +305,14 @@ func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, conf
// configV will always be null in this case, by definition.
// priorV may also be null, but that's okay.
newV = priorV

// the exception to the above is that if the config is optional and
// the _prior_ value contains non-computed values, we can infer
// that the config must have been non-null previously.
if optionalValueNotComputable(attr, priorV) {
newV = configV
}

case attr.NestedType != nil:
// For non-computed NestedType attributes, we need to descend
// into the individual nested attributes to build the final
Expand All @@ -316,7 +332,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
}

Expand All @@ -332,7 +348,7 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
break
}

newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
newV = proposedNewObjectAttributes(schema.Attributes, prior, config)

case configschema.NestingList:
// Nested blocks are correlated by index.
Expand All @@ -353,8 +369,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.Attributes, priorEV, configEV)
newVals = append(newVals, newEV)
}
// Despite the name, a NestingList might also be a tuple, if
// its nested schema contains dynamically-typed attributes.
Expand All @@ -366,58 +382,55 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
}

case configschema.NestingMap:
configVLen := 0
if config.IsKnown() && !config.IsNull() {
configVLen = config.LengthInt()
}

if configVLen == 0 {
break
}

newVals := make(map[string]cty.Value, configVLen)

// 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)
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
}
// 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)
priorEV := prior.GetAttr(name)
newEV := proposedNewObjectAttributes(schema.Attributes, 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 {
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)

newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
newVals[k] = cty.ObjectVal(newEV)
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
}
newV = cty.MapVal(newVals)
priorEV := prior.Index(idx)

newEV := proposedNewObjectAttributes(schema.Attributes, priorEV, configEV)
newVals[k] = newEV
}
newV = cty.MapVal(newVals)
}

case configschema.NestingSet:
Expand Down Expand Up @@ -453,8 +466,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.Attributes, priorEV, configEV)
newVals = append(newVals, newEV)
}
}
newV = cty.SetVal(newVals)
Expand Down Expand Up @@ -518,3 +531,43 @@ func setElementComputedAsNull(schema attrPath, elem cty.Value) cty.Value {

return elem
}

// optionalValueNotComputable is used to check if an object in state must
// have at least partially come from configuration. If the prior value has any
// non-null attributes which are not computed in the schema, then we know there
// was previously a configuration value which set those.
//
// This is used when the configuration contains a null optional+computed value,
// and we want to know if we should plan to send the null value or the prior
// state.
func optionalValueNotComputable(schema *configschema.Attribute, val cty.Value) bool {
if !schema.Optional {
return false
}

// We must have a NestedType for complex nested attributes in order
// to find nested computed values in the first place.
if schema.NestedType == nil {
return false
}

foundNonComputedAttr := false
cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) {
if v.IsNull() {
return true, nil
}

attr := schema.NestedType.AttributeByPath(path)
if attr == nil {
return true, nil
}

if !attr.Computed {
foundNonComputedAttr = true
return false, nil
}
return true, nil
})

return foundNonComputedAttr
}
134 changes: 124 additions & 10 deletions internal/plans/objchange/objchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,10 +460,10 @@ func TestProposedNew(t *testing.T) {
})),
}),
cty.ObjectVal(map[string]cty.Value{
"bloop": cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("glub"),
"bleep": cty.NullVal(cty.String),
}),
"bloop": cty.NullVal(cty.Object(map[string]cty.Type{
"blop": cty.String,
"bleep": cty.String,
})),
}),
},

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -2036,14 +2150,14 @@ func TestProposedNew(t *testing.T) {
)),
}),
cty.ObjectVal(map[string]cty.Value{
"list_obj": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"obj": cty.ObjectVal(map[string]cty.Value{
"optional": cty.StringVal("prior"),
"computed": cty.StringVal("prior computed"),
"list_obj": cty.NullVal(cty.List(
cty.Object(map[string]cty.Type{
"obj": cty.Object(map[string]cty.Type{
"optional": cty.String,
"computed": cty.String,
}),
}),
}),
)),
}),
},

Expand Down

0 comments on commit 0462b84

Please # to comment.