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: 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.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 { case configschema.NestingSingle: 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 { attrs[name] = cty.TupleVal(elems) } } else { if blockType.Nesting == configschema.NestingSet { attrs[name] = cty.SetValEmpty(blockType.Block.ImpliedType()) } else { attrs[name] = cty.EmptyTupleVal } } 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) } attrs[name] = cty.ObjectVal(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) }