| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package objchange | |
| import ( | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/zclconf/go-cty/cty" | |
| ) | |
| // NormalizeObjectFromLegacySDK takes an object that may have been generated by | |
| // the legacy Terraform SDK (i.e. returned from a provider with the | |
| // LegacyTypeSystem opt-out set) and does its best to normalize it for the | |
| // assumptions we would normally enforce if the provider had not opted out. | |
| // | |
| // In particular, this function guarantees that a value representing a nested | |
| // block will never itself be unknown or null, instead representing that as a | |
| // non-null value that may contain null/unknown values. | |
| // | |
| // The input value must still conform to the implied type of the given schema, | |
| // or else this function may produce garbage results or panic. This is usually | |
| // okay because type consistency is enforced when deserializing the value | |
| // returned from the provider over the RPC wire protocol anyway. | |
| // | |
| // This is clearly incompatible with computed blocks in protocol v6, so must not | |
| // be called for anything using features from v6 or greater. | |
| func NormalizeObjectFromLegacySDK(val cty.Value, schema *configschema.Block) cty.Value { | |
| val, valMarks := val.UnmarkDeepWithPaths() | |
| val = normalizeObjectFromLegacySDK(val, schema) | |
| return val.MarkWithPaths(valMarks) | |
| } | |
| func normalizeObjectFromLegacySDK(val cty.Value, schema *configschema.Block) cty.Value { | |
| if val == cty.NilVal || val.IsNull() { | |
| // This should never happen in reasonable use, but we'll allow it | |
| // and normalize to a null of the expected type rather than panicking | |
| // below. | |
| return cty.NullVal(schema.ImpliedType()) | |
| } | |
| vals := make(map[string]cty.Value) | |
| for name := range schema.Attributes { | |
| // No normalization for attributes, since them being type-conformant | |
| // is all that we require. | |
| vals[name] = val.GetAttr(name) | |
| } | |
| for name, blockS := range schema.BlockTypes { | |
| lv := val.GetAttr(name) | |
| // Legacy SDK never generates dynamically-typed attributes and so our | |
| // normalization code doesn't deal with them, but we need to make sure | |
| // we still pass them through properly so that we don't interfere with | |
| // objects generated by other SDKs. | |
| if ty := blockS.Block.ImpliedType(); ty.HasDynamicTypes() { | |
| vals[name] = lv | |
| continue | |
| } | |
| switch blockS.Nesting { | |
| case configschema.NestingSingle, configschema.NestingGroup: | |
| if lv.IsKnown() { | |
| if lv.IsNull() && blockS.Nesting == configschema.NestingGroup { | |
| vals[name] = blockS.EmptyValue() | |
| } else { | |
| vals[name] = normalizeObjectFromLegacySDK(lv, &blockS.Block) | |
| } | |
| } else { | |
| vals[name] = unknownBlockStub(&blockS.Block) | |
| } | |
| case configschema.NestingList: | |
| switch { | |
| case !lv.IsKnown(): | |
| vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)}) | |
| case lv.IsNull() || lv.LengthInt() == 0: | |
| vals[name] = cty.ListValEmpty(blockS.Block.ImpliedType()) | |
| default: | |
| subVals := make([]cty.Value, 0, lv.LengthInt()) | |
| for it := lv.ElementIterator(); it.Next(); { | |
| _, subVal := it.Element() | |
| subVals = append(subVals, normalizeObjectFromLegacySDK(subVal, &blockS.Block)) | |
| } | |
| vals[name] = cty.ListVal(subVals) | |
| } | |
| case configschema.NestingSet: | |
| switch { | |
| case !lv.IsKnown(): | |
| vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)}) | |
| case lv.IsNull() || lv.LengthInt() == 0: | |
| vals[name] = cty.SetValEmpty(blockS.Block.ImpliedType()) | |
| default: | |
| subVals := make([]cty.Value, 0, lv.LengthInt()) | |
| for it := lv.ElementIterator(); it.Next(); { | |
| _, subVal := it.Element() | |
| subVals = append(subVals, normalizeObjectFromLegacySDK(subVal, &blockS.Block)) | |
| } | |
| vals[name] = cty.SetVal(subVals) | |
| } | |
| default: | |
| // The legacy SDK doesn't support NestingMap, so we just assume | |
| // maps are always okay. (If not, we would've detected and returned | |
| // an error to the user before we got here.) | |
| vals[name] = lv | |
| } | |
| } | |
| return cty.ObjectVal(vals) | |
| } | |
| // unknownBlockStub constructs an object value that approximates an unknown | |
| // block by producing a known block object with all of its leaf attribute | |
| // values set to unknown. | |
| // | |
| // Blocks themselves cannot be unknown, so if the legacy SDK tries to return | |
| // such a thing, we'll use this result instead. This convention mimics how | |
| // the dynamic block feature deals with being asked to iterate over an unknown | |
| // value, because our value-checking functions already accept this convention | |
| // as a special case. | |
| func unknownBlockStub(schema *configschema.Block) cty.Value { | |
| vals := make(map[string]cty.Value) | |
| for name, attrS := range schema.Attributes { | |
| vals[name] = cty.UnknownVal(attrS.Type) | |
| } | |
| for name, blockS := range schema.BlockTypes { | |
| switch blockS.Nesting { | |
| case configschema.NestingSingle, configschema.NestingGroup: | |
| vals[name] = unknownBlockStub(&blockS.Block) | |
| case configschema.NestingList: | |
| // In principle we may be expected to produce a tuple value here, | |
| // if there are any dynamically-typed attributes in our nested block, | |
| // but the legacy SDK doesn't support that, so we just assume it'll | |
| // never be necessary to normalize those. (Incorrect usage in any | |
| // other SDK would be caught and returned as an error before we | |
| // get here.) | |
| vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)}) | |
| case configschema.NestingSet: | |
| vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)}) | |
| case configschema.NestingMap: | |
| // A nesting map can never be unknown since we then wouldn't know | |
| // what the keys are. (Legacy SDK doesn't support NestingMap anyway, | |
| // so this should never arise.) | |
| vals[name] = cty.MapValEmpty(blockS.Block.ImpliedType()) | |
| } | |
| } | |
| return cty.ObjectVal(vals) | |
| } |