terraform/plans/objchange/objchange.go

403 lines
14 KiB
Go
Raw Normal View History

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() {
2019-02-07 23:01:39 +01:00
// 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 {
configs/configschema: Introduce the NestingGroup mode for blocks In study of existing providers we've found a pattern we werent previously accounting for of using a nested block type to represent a group of arguments that relate to a particular feature that is always enabled but where it improves configuration readability to group all of its settings together in a nested block. The existing NestingSingle was not a good fit for this because it is designed under the assumption that the presence or absence of the block has some significance in enabling or disabling the relevant feature, and so for these always-active cases we'd generate a misleading plan where the settings for the feature appear totally absent, rather than showing the default values that will be selected. NestingGroup is, therefore, a slight variation of NestingSingle where presence vs. absence of the block is not distinguishable (it's never null) and instead its contents are treated as unset when the block is absent. This then in turn causes any default values associated with the nested arguments to be honored and displayed in the plan whenever the block is not explicitly configured. The current SDK cannot activate this mode, but that's okay because its "legacy type system" opt-out flag allows it to force a block to be processed in this way anyway. We're adding this now so that we can introduce the feature in a future SDK without causing a breaking change to the protocol, since the set of possible block nesting modes is not extensible.
2019-04-09 00:32:53 +02:00
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 {
switch blockType.Nesting {
configs/configschema: Introduce the NestingGroup mode for blocks In study of existing providers we've found a pattern we werent previously accounting for of using a nested block type to represent a group of arguments that relate to a particular feature that is always enabled but where it improves configuration readability to group all of its settings together in a nested block. The existing NestingSingle was not a good fit for this because it is designed under the assumption that the presence or absence of the block has some significance in enabling or disabling the relevant feature, and so for these always-active cases we'd generate a misleading plan where the settings for the feature appear totally absent, rather than showing the default values that will be selected. NestingGroup is, therefore, a slight variation of NestingSingle where presence vs. absence of the block is not distinguishable (it's never null) and instead its contents are treated as unset when the block is absent. This then in turn causes any default values associated with the nested arguments to be honored and displayed in the plan whenever the block is not explicitly configured. The current SDK cannot activate this mode, but that's okay because its "legacy type system" opt-out flag allows it to force a block to be processed in this way anyway. We're adding this now so that we can introduce the feature in a future SDK without causing a breaking change to the protocol, since the set of possible block nesting modes is not extensible.
2019-04-09 00:32:53 +02:00
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))
}
if 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)
} else {
if blockType.Block.ImpliedType().HasDynamicTypes() {
attrs[name] = cty.TupleVal(elems)
} else {
attrs[name] = cty.ListVal(elems)
}
}
} else {
if blockType.Nesting == configschema.NestingSet {
attrs[name] = cty.SetValEmpty(blockType.Block.ImpliedType())
} else {
if blockType.Block.ImpliedType().HasDynamicTypes() {
attrs[name] = cty.EmptyTupleVal
} else {
attrs[name] = cty.ListValEmpty(blockType.Block.ImpliedType())
}
}
}
case configschema.NestingMap:
cv := v.GetAttr(name)
if cv.IsNull() || !cv.IsKnown() {
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)
}
if blockType.Block.ImpliedType().HasDynamicTypes() {
attrs[name] = cty.ObjectVal(elems)
} else {
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)
}