Skip to content

Commit

Permalink
better handling of nested sets in objchange
Browse files Browse the repository at this point in the history
Combine and simplify the set comparison functions for NestingSet blocks
and attribute types.

The set handling for structural attributes was not recursing into nested
values. Once a simplified method for comparing set elements was devised
for nested types, it turns out the same method could be applied to
nested set blocks as well.
  • Loading branch information
jbardin committed Jan 19, 2023
1 parent ac4f5fe commit 048ee34
Showing 1 changed file with 35 additions and 154 deletions.
189 changes: 35 additions & 154 deletions internal/plans/objchange/objchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
var newV cty.Value

switch schema.Nesting {

case configschema.NestingSingle:
// A NestingSingle configuration block value can be null, and since it
// cannot be computed we can always take the configuration value.
Expand Down Expand Up @@ -250,6 +249,7 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
if used[i] {
continue
}

if cmp[1].RawEquals(configEV) {
priorEV = cmp[0]
used[i] = true // we can't use this value on a future iteration
Expand Down Expand Up @@ -287,18 +287,13 @@ func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, conf

configV := config.GetAttr(name)

var newV cty.Value
switch {
// required isn't considered when constructing the plan, so attributes
// are essentially either computed or not computed. In the case of
// optional+computed, they are only computed when there is no
// configuration.
computed := attr.Computed
if attr.Optional && !configV.IsNull() {
computed = false
}

var newV cty.Value
switch {
case computed:
case attr.Computed && configV.IsNull():
// configV will always be null in this case, by definition.
// priorV may also be null, but that's okay.
newV = priorV
Expand Down Expand Up @@ -329,12 +324,13 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)

switch schema.Nesting {
case configschema.NestingSingle:
if !config.IsNull() {
newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
} else {
newV = cty.NullVal(config.Type())
// if the config is null, we already have our value
if config.IsNull() {
break
}

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

case configschema.NestingList:
// Nested blocks are correlated by index.
configVLen := 0
Expand Down Expand Up @@ -429,7 +425,7 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
// if the non-computed attribute values are identical.
var cmpVals [][2]cty.Value
if prior.IsKnown() && !prior.IsNull() {
cmpVals = setElementCompareValuesFromObject(schema, prior)
cmpVals = setElementCompareValues(schema, prior)
}
configVLen := 0
if config.IsKnown() && !config.IsNull() {
Expand Down Expand Up @@ -479,158 +475,43 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
// in proposedNewBlock. The result is a heuristic rather than an exact science,
// since e.g. two separate elements may reduce to the same value through this
// process. The caller must therefore be ready to deal with duplicates.
func setElementCompareValues(schema *configschema.Block, set cty.Value) [][2]cty.Value {
func setElementCompareValues(schema attrPath, set cty.Value) [][2]cty.Value {
ret := make([][2]cty.Value, 0, set.LengthInt())
for it := set.ElementIterator(); it.Next(); {
_, ev := it.Element()
ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev)})
ret = append(ret, [2]cty.Value{ev, setElementComputedAsNull(schema, ev)})
}
return ret
}

// setElementCompareValue creates a new value that has all of the same
// non-computed attribute values as the one given but has all computed
// attribute values forced to null.
//
// The input value must conform to the schema's implied type, and the return
// value is guaranteed to conform to it.
func setElementCompareValue(schema *configschema.Block, v cty.Value) cty.Value {
if v.IsNull() || !v.IsKnown() {
return v
}
type attrPath interface {
AttributeByPath(cty.Path) *configschema.Attribute
}

attrs := map[string]cty.Value{}
for name, attr := range schema.Attributes {
switch {
case attr.Computed && attr.Optional:
attrs[name] = cty.NullVal(attr.ImpliedType())
case attr.Computed:
attrs[name] = cty.NullVal(attr.ImpliedType())
default:
attrs[name] = v.GetAttr(name)
func setElementComputedAsNull(schema attrPath, elem cty.Value) cty.Value {
elem, _ = cty.Transform(elem, func(path cty.Path, v cty.Value) (cty.Value, error) {
if v.IsNull() || !v.IsKnown() {
return v, nil
}
}

for name, blockType := range schema.BlockTypes {
elementType := blockType.Block.ImpliedType()

switch blockType.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name))

case configschema.NestingList, configschema.NestingSet:
cv := v.GetAttr(name)
if cv.IsNull() || !cv.IsKnown() {
attrs[name] = cv
continue
}

if l := cv.LengthInt(); l > 0 {
elems := make([]cty.Value, 0, l)
for it := cv.ElementIterator(); it.Next(); {
_, ev := it.Element()
elems = append(elems, setElementCompareValue(&blockType.Block, ev))
}

switch {
case blockType.Nesting == configschema.NestingSet:
// SetValEmpty would panic if given elements that are not
// all of the same type, but that's guaranteed not to
// happen here because our input value was _already_ a
// set and we've not changed the types of any elements here.
attrs[name] = cty.SetVal(elems)

// NestingList cases
case elementType.HasDynamicTypes():
attrs[name] = cty.TupleVal(elems)
default:
attrs[name] = cty.ListVal(elems)
}
} else {
switch {
case blockType.Nesting == configschema.NestingSet:
attrs[name] = cty.SetValEmpty(elementType)

// NestingList cases
case elementType.HasDynamicTypes():
attrs[name] = cty.EmptyTupleVal
default:
attrs[name] = cty.ListValEmpty(elementType)
}
}

case configschema.NestingMap:
cv := v.GetAttr(name)
if cv.IsNull() || !cv.IsKnown() || cv.LengthInt() == 0 {
attrs[name] = cv
continue
}
elems := make(map[string]cty.Value)
for it := cv.ElementIterator(); it.Next(); {
kv, ev := it.Element()
elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev)
}

switch {
case elementType.HasDynamicTypes():
attrs[name] = cty.ObjectVal(elems)
default:
attrs[name] = cty.MapVal(elems)
}

default:
// Should never happen, since the above cases are comprehensive.
panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
attr := schema.AttributeByPath(path)
if attr == nil {
// we must be at a nested block rather than an attribute, continue
return v, nil
}
}

return cty.ObjectVal(attrs)
}

// setElementCompareValues takes a known, non-null value of a cty.Set type and
// returns a table -- constructed of two-element arrays -- that maps original
// set element values to corresponding values that have all of the computed
// values removed, making them suitable for comparison with values obtained
// from configuration. The element type of the set must conform to the implied
// type of the given schema, or this function will panic.
//
// In the resulting slice, the zeroth element of each array is the original
// value and the one-indexed element is the corresponding "compare value".
//
// This is intended to help correlate prior elements with configured elements
// in proposedNewBlock. The result is a heuristic rather than an exact science,
// since e.g. two separate elements may reduce to the same value through this
// process. The caller must therefore be ready to deal with duplicates.
func setElementCompareValuesFromObject(schema *configschema.Object, set cty.Value) [][2]cty.Value {
ret := make([][2]cty.Value, 0, set.LengthInt())
for it := set.ElementIterator(); it.Next(); {
_, ev := it.Element()
ret = append(ret, [2]cty.Value{ev, setElementCompareValueFromObject(schema, ev)})
}
return ret
}

// setElementCompareValue creates a new value that has all of the same
// non-computed attribute values as the one given but has all computed
// attribute values forced to null.
//
// The input value must conform to the schema's implied type, and the return
// value is guaranteed to conform to it.
func setElementCompareValueFromObject(schema *configschema.Object, v cty.Value) cty.Value {
if v.IsNull() || !v.IsKnown() {
return v
}
attrs := map[string]cty.Value{}

for name, attr := range schema.Attributes {
attrV := v.GetAttr(name)
switch {
case attr.Computed:
attrs[name] = cty.NullVal(attr.Type)
default:
attrs[name] = attrV
if attr.Computed {
// If this could have been computed, we always return null in order
// to compare config and state values.
//
// The only way to determine if an optional+computed value is
// optional or computed is see if it is set in the config, however
// for set elements we don't yet know which element value
// corresponds to the configuration.
return cty.NullVal(v.Type()), nil
}
}
return v, nil
})

return cty.ObjectVal(attrs)
return elem
}

0 comments on commit 048ee34

Please sign in to comment.