| package objchange |
| |
| import ( |
| "fmt" |
| |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| ) |
| |
| // ProposedNew 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 ProposedNew(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. |
| // |
| // "All attributes null" happens to be the definition of EmptyValue for |
| // a Block, so we can just delegate to that |
| prior = schema.EmptyValue() |
| } |
| return proposedNew(schema, prior, config) |
| } |
| |
| // PlannedDataResourceObject is similar to proposedNewBlock 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 proposedNewBlock 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 proposedNewBlock 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 proposedNew(schema, prior, config) |
| } |
| |
| func proposedNew(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("ProposedNew 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 := proposedNewAttributes(schema.Attributes, prior, config) |
| |
| // 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) |
| newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV) |
| } |
| |
| return cty.ObjectVal(newAttrs) |
| } |
| |
| func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value { |
| // The only time we should encounter an entirely unknown block is from the |
| // use of dynamic with an unknown for_each expression. |
| if !config.IsKnown() { |
| return config |
| } |
| |
| var newV cty.Value |
| |
| switch schema.Nesting { |
| |
| case configschema.NestingSingle, configschema.NestingGroup: |
| newV = ProposedNew(&schema.Block, prior, config) |
| |
| case configschema.NestingList: |
| // Nested blocks are correlated by index. |
| configVLen := 0 |
| if !config.IsNull() { |
| configVLen = config.LengthInt() |
| } |
| if configVLen > 0 { |
| newVals := make([]cty.Value, 0, configVLen) |
| for it := config.ElementIterator(); it.Next(); { |
| idx, configEV := it.Element() |
| 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 = append(newVals, configEV) |
| continue |
| } |
| priorEV := prior.Index(idx) |
| |
| newEV := ProposedNew(&schema.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 config.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 config.Type().IsTupleType() { |
| newV = cty.EmptyTupleVal |
| } else { |
| newV = cty.ListValEmpty(schema.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 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 := ProposedNew(&schema.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 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 := ProposedNew(&schema.Block, priorEV, configEV) |
| newVals[k] = newEV |
| } |
| newV = cty.MapVal(newVals) |
| } else { |
| newV = cty.MapValEmpty(schema.ImpliedType()) |
| } |
| } |
| |
| case configschema.NestingSet: |
| if !config.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 prior.IsKnown() && !prior.IsNull() { |
| cmpVals = setElementCompareValues(&schema.Block, prior, false) |
| } |
| configVLen := 0 |
| if config.IsKnown() && !config.IsNull() { |
| configVLen = config.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 := config.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(schema.ImpliedType()) |
| } |
| |
| newEV := ProposedNew(&schema.Block, priorEV, configEV) |
| newVals = append(newVals, newEV) |
| } |
| newV = cty.SetVal(newVals) |
| } else { |
| newV = cty.SetValEmpty(schema.Block.ImpliedType()) |
| } |
| |
| default: |
| // Should never happen, since the above cases are comprehensive. |
| panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting)) |
| } |
| return newV |
| } |
| |
| 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 { |
| var priorV cty.Value |
| if prior.IsNull() { |
| priorV = cty.NullVal(prior.Type().AttributeType(name)) |
| } else { |
| 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: |
| if attr.NestedType != nil { |
| // For non-computed NestedType attributes, we need to descend |
| // into the individual nested attributes to build the final |
| // value, unless the entire nested attribute is unknown. |
| if !configV.IsKnown() { |
| newV = configV |
| } else { |
| newV = proposedNewNestedType(attr.NestedType, priorV, configV) |
| } |
| } else { |
| // 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 |
| } |
| return newAttrs |
| } |
| |
| func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value { |
| // If the config is null or empty, we will be using this default value. |
| newV := config |
| |
| switch schema.Nesting { |
| case configschema.NestingSingle: |
| if !config.IsNull() { |
| newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config)) |
| } else { |
| newV = cty.NullVal(config.Type()) |
| } |
| |
| case configschema.NestingList: |
| // Nested blocks are correlated by index. |
| configVLen := 0 |
| if config.IsKnown() && !config.IsNull() { |
| configVLen = config.LengthInt() |
| } |
| |
| if configVLen > 0 { |
| newVals := make([]cty.Value, 0, configVLen) |
| for it := config.ElementIterator(); it.Next(); { |
| idx, configEV := it.Element() |
| 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 = append(newVals, configEV) |
| continue |
| } |
| priorEV := prior.Index(idx) |
| |
| newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) |
| newVals = append(newVals, cty.ObjectVal(newEV)) |
| } |
| // Despite the name, a NestingList might also be a tuple, if |
| // its nested schema contains dynamically-typed attributes. |
| if config.Type().IsTupleType() { |
| newV = cty.TupleVal(newVals) |
| } else { |
| newV = cty.ListVal(newVals) |
| } |
| } |
| |
| 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 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) |
| } |
| // 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) |
| } |
| newV = cty.MapVal(newVals) |
| } |
| } |
| |
| case configschema.NestingSet: |
| // 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 prior.IsKnown() && !prior.IsNull() { |
| cmpVals = setElementCompareValuesFromObject(schema, prior) |
| } |
| configVLen := 0 |
| if config.IsKnown() && !config.IsNull() { |
| configVLen = config.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 := config.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 { |
| newVals = append(newVals, configEV) |
| } else { |
| newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) |
| newVals = append(newVals, cty.ObjectVal(newEV)) |
| } |
| } |
| newV = cty.SetVal(newVals) |
| } |
| } |
| |
| return newV |
| } |
| |
| // 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 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) |
| } |
| |
| // 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 |
| } |
| } |
| |
| return cty.ObjectVal(attrs) |
| } |