410 lines
14 KiB
Go
410 lines
14 KiB
Go
package objchange
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
|
)
|
|
|
|
// ProposedNewObject constructs a proposed new object value by combining the
|
|
// computed attribute values from "prior" with the configured attribute values
|
|
// from "config".
|
|
//
|
|
// Both value must conform to the given schema's implied type, or this function
|
|
// will panic.
|
|
//
|
|
// The prior value must be wholly known, but the config value may be unknown
|
|
// or have nested unknown values.
|
|
//
|
|
// The merging of the two objects includes the attributes of any nested blocks,
|
|
// which will be correlated in a manner appropriate for their nesting mode.
|
|
// Note in particular that the correlation for blocks backed by sets is a
|
|
// heuristic based on matching non-computed attribute values and so it may
|
|
// produce strange results with more "extreme" cases, such as a nested set
|
|
// block where _all_ attributes are computed.
|
|
func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value {
|
|
// If the config and prior are both null, return early here before
|
|
// populating the prior block. The prevents non-null blocks from appearing
|
|
// the proposed state value.
|
|
if config.IsNull() && prior.IsNull() {
|
|
return prior
|
|
}
|
|
|
|
if prior.IsNull() {
|
|
// In this case, we will construct a synthetic prior value that is
|
|
// similar to the result of decoding an empty configuration block,
|
|
// which simplifies our handling of the top-level attributes/blocks
|
|
// below by giving us one non-null level of object to pull values from.
|
|
prior = AllAttributesNull(schema)
|
|
}
|
|
return proposedNewObject(schema, prior, config)
|
|
}
|
|
|
|
// PlannedDataResourceObject is similar to ProposedNewObject but tailored for
|
|
// planning data resources in particular. Specifically, it replaces the values
|
|
// of any Computed attributes not set in the configuration with an unknown
|
|
// value, which serves as a placeholder for a value to be filled in by the
|
|
// provider when the data resource is finally read.
|
|
//
|
|
// Data resources are different because the planning of them is handled
|
|
// entirely within Terraform Core and not subject to customization by the
|
|
// provider. This function is, in effect, producing an equivalent result to
|
|
// passing the ProposedNewObject result into a provider's PlanResourceChange
|
|
// function, assuming a fixed implementation of PlanResourceChange that just
|
|
// fills in unknown values as needed.
|
|
func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
|
|
// Our trick here is to run the ProposedNewObject logic with an
|
|
// entirely-unknown prior value. Because of cty's unknown short-circuit
|
|
// behavior, any operation on prior returns another unknown, and so
|
|
// unknown values propagate into all of the parts of the resulting value
|
|
// that would normally be filled in by preserving the prior state.
|
|
prior := cty.UnknownVal(schema.ImpliedType())
|
|
return proposedNewObject(schema, prior, config)
|
|
}
|
|
|
|
func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value {
|
|
if config.IsNull() || !config.IsKnown() {
|
|
// This is a weird situation, but we'll allow it anyway to free
|
|
// callers from needing to specifically check for these cases.
|
|
return prior
|
|
}
|
|
if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) {
|
|
panic("ProposedNewObject only supports object-typed values")
|
|
}
|
|
|
|
// From this point onwards, we can assume that both values are non-null
|
|
// object types, and that the config value itself is known (though it
|
|
// may contain nested values that are unknown.)
|
|
|
|
newAttrs := map[string]cty.Value{}
|
|
for name, attr := range schema.Attributes {
|
|
priorV := prior.GetAttr(name)
|
|
configV := config.GetAttr(name)
|
|
var newV cty.Value
|
|
switch {
|
|
case attr.Computed && attr.Optional:
|
|
// This is the trickiest scenario: we want to keep the prior value
|
|
// if the config isn't overriding it. Note that due to some
|
|
// ambiguity here, setting an optional+computed attribute from
|
|
// config and then later switching the config to null in a
|
|
// subsequent change causes the initial config value to be "sticky"
|
|
// unless the provider specifically overrides it during its own
|
|
// plan customization step.
|
|
if configV.IsNull() {
|
|
newV = priorV
|
|
} else {
|
|
newV = configV
|
|
}
|
|
case attr.Computed:
|
|
// configV will always be null in this case, by definition.
|
|
// priorV may also be null, but that's okay.
|
|
newV = priorV
|
|
default:
|
|
// For non-computed attributes, we always take the config value,
|
|
// even if it is null. If it's _required_ then null values
|
|
// should've been caught during an earlier validation step, and
|
|
// so we don't really care about that here.
|
|
newV = configV
|
|
}
|
|
newAttrs[name] = newV
|
|
}
|
|
|
|
// Merging nested blocks is a little more complex, since we need to
|
|
// correlate blocks between both objects and then recursively propose
|
|
// a new object for each. The correlation logic depends on the nesting
|
|
// mode for each block type.
|
|
for name, blockType := range schema.BlockTypes {
|
|
priorV := prior.GetAttr(name)
|
|
configV := config.GetAttr(name)
|
|
var newV cty.Value
|
|
switch blockType.Nesting {
|
|
|
|
case configschema.NestingSingle, configschema.NestingGroup:
|
|
newV = ProposedNewObject(&blockType.Block, priorV, configV)
|
|
|
|
case configschema.NestingList:
|
|
// Nested blocks are correlated by index.
|
|
configVLen := 0
|
|
if configV.IsKnown() && !configV.IsNull() {
|
|
configVLen = configV.LengthInt()
|
|
}
|
|
if configVLen > 0 {
|
|
newVals := make([]cty.Value, 0, configVLen)
|
|
for it := configV.ElementIterator(); it.Next(); {
|
|
idx, configEV := it.Element()
|
|
if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) {
|
|
// If there is no corresponding prior element then
|
|
// we just take the config value as-is.
|
|
newVals = append(newVals, configEV)
|
|
continue
|
|
}
|
|
priorEV := priorV.Index(idx)
|
|
|
|
newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
|
|
newVals = append(newVals, newEV)
|
|
}
|
|
// Despite the name, a NestingList might also be a tuple, if
|
|
// its nested schema contains dynamically-typed attributes.
|
|
if configV.Type().IsTupleType() {
|
|
newV = cty.TupleVal(newVals)
|
|
} else {
|
|
newV = cty.ListVal(newVals)
|
|
}
|
|
} else {
|
|
// Despite the name, a NestingList might also be a tuple, if
|
|
// its nested schema contains dynamically-typed attributes.
|
|
if configV.Type().IsTupleType() {
|
|
newV = cty.EmptyTupleVal
|
|
} else {
|
|
newV = cty.ListValEmpty(blockType.ImpliedType())
|
|
}
|
|
}
|
|
|
|
case configschema.NestingMap:
|
|
// Despite the name, a NestingMap may produce either a map or
|
|
// object value, depending on whether the nested schema contains
|
|
// dynamically-typed attributes.
|
|
if configV.Type().IsObjectType() {
|
|
// Nested blocks are correlated by key.
|
|
configVLen := 0
|
|
if configV.IsKnown() && !configV.IsNull() {
|
|
configVLen = configV.LengthInt()
|
|
}
|
|
if configVLen > 0 {
|
|
newVals := make(map[string]cty.Value, configVLen)
|
|
atys := configV.Type().AttributeTypes()
|
|
for name := range atys {
|
|
configEV := configV.GetAttr(name)
|
|
if !priorV.IsKnown() || priorV.IsNull() || !priorV.Type().HasAttribute(name) {
|
|
// If there is no corresponding prior element then
|
|
// we just take the config value as-is.
|
|
newVals[name] = configEV
|
|
continue
|
|
}
|
|
priorEV := priorV.GetAttr(name)
|
|
|
|
newEV := ProposedNewObject(&blockType.Block, 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 {
|
|
newV = cty.EmptyObjectVal
|
|
}
|
|
} else {
|
|
configVLen := 0
|
|
if configV.IsKnown() && !configV.IsNull() {
|
|
configVLen = configV.LengthInt()
|
|
}
|
|
if configVLen > 0 {
|
|
newVals := make(map[string]cty.Value, configVLen)
|
|
for it := configV.ElementIterator(); it.Next(); {
|
|
idx, configEV := it.Element()
|
|
k := idx.AsString()
|
|
if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) {
|
|
// If there is no corresponding prior element then
|
|
// we just take the config value as-is.
|
|
newVals[k] = configEV
|
|
continue
|
|
}
|
|
priorEV := priorV.Index(idx)
|
|
|
|
newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
|
|
newVals[k] = newEV
|
|
}
|
|
newV = cty.MapVal(newVals)
|
|
} else {
|
|
newV = cty.MapValEmpty(blockType.ImpliedType())
|
|
}
|
|
}
|
|
|
|
case configschema.NestingSet:
|
|
if !configV.Type().IsSetType() {
|
|
panic("configschema.NestingSet value is not a set as expected")
|
|
}
|
|
|
|
// Nested blocks are correlated by comparing the element values
|
|
// after eliminating all of the computed attributes. In practice,
|
|
// this means that any config change produces an entirely new
|
|
// nested object, and we only propagate prior computed values
|
|
// if the non-computed attribute values are identical.
|
|
var cmpVals [][2]cty.Value
|
|
if priorV.IsKnown() && !priorV.IsNull() {
|
|
cmpVals = setElementCompareValues(&blockType.Block, priorV, false)
|
|
}
|
|
configVLen := 0
|
|
if configV.IsKnown() && !configV.IsNull() {
|
|
configVLen = configV.LengthInt()
|
|
}
|
|
if configVLen > 0 {
|
|
used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
|
|
newVals := make([]cty.Value, 0, configVLen)
|
|
for it := configV.ElementIterator(); it.Next(); {
|
|
_, configEV := it.Element()
|
|
var priorEV cty.Value
|
|
for i, cmp := range cmpVals {
|
|
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
|
|
break
|
|
}
|
|
}
|
|
if priorEV == cty.NilVal {
|
|
priorEV = cty.NullVal(blockType.ImpliedType())
|
|
}
|
|
|
|
newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
|
|
newVals = append(newVals, newEV)
|
|
}
|
|
newV = cty.SetVal(newVals)
|
|
} else {
|
|
newV = cty.SetValEmpty(blockType.Block.ImpliedType())
|
|
}
|
|
|
|
default:
|
|
// Should never happen, since the above cases are comprehensive.
|
|
panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
|
|
}
|
|
|
|
newAttrs[name] = newV
|
|
}
|
|
|
|
return cty.ObjectVal(newAttrs)
|
|
}
|
|
|
|
// 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 ProposedNewObject. 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, isConfig bool) [][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, isConfig)})
|
|
}
|
|
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.
|
|
//
|
|
// If isConfig is true then non-null Optional+Computed attribute values will
|
|
// be preserved. Otherwise, they will also be set 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, isConfig bool) cty.Value {
|
|
if v.IsNull() || !v.IsKnown() {
|
|
return v
|
|
}
|
|
|
|
attrs := map[string]cty.Value{}
|
|
for name, attr := range schema.Attributes {
|
|
switch {
|
|
case attr.Computed && attr.Optional:
|
|
if isConfig {
|
|
attrs[name] = v.GetAttr(name)
|
|
} else {
|
|
attrs[name] = cty.NullVal(attr.Type)
|
|
}
|
|
case attr.Computed:
|
|
attrs[name] = cty.NullVal(attr.Type)
|
|
default:
|
|
attrs[name] = v.GetAttr(name)
|
|
}
|
|
}
|
|
|
|
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), isConfig)
|
|
|
|
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, isConfig))
|
|
}
|
|
|
|
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, isConfig)
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
return cty.ObjectVal(attrs)
|
|
}
|