github.com/opentofu/opentofu@v1.7.1/internal/plans/objchange/normalize_obj.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package objchange 7 8 import ( 9 "github.com/opentofu/opentofu/internal/configs/configschema" 10 "github.com/zclconf/go-cty/cty" 11 ) 12 13 // NormalizeObjectFromLegacySDK takes an object that may have been generated 14 // by the legacy Terraform SDK (i.e. returned from a provider with the 15 // LegacyTypeSystem opt-out set) and does its best to normalize it for the 16 // assumptions we would normally enforce if the provider had not opted out. 17 // 18 // In particular, this function guarantees that a value representing a nested 19 // block will never itself be unknown or null, instead representing that as 20 // a non-null value that may contain null/unknown values. 21 // 22 // The input value must still conform to the implied type of the given schema, 23 // or else this function may produce garbage results or panic. This is usually 24 // okay because type consistency is enforced when deserializing the value 25 // returned from the provider over the RPC wire protocol anyway. 26 func NormalizeObjectFromLegacySDK(val cty.Value, schema *configschema.Block) cty.Value { 27 val, valMarks := val.UnmarkDeepWithPaths() 28 val = normalizeObjectFromLegacySDK(val, schema) 29 return val.MarkWithPaths(valMarks) 30 } 31 32 func normalizeObjectFromLegacySDK(val cty.Value, schema *configschema.Block) cty.Value { 33 if val == cty.NilVal || val.IsNull() { 34 // This should never happen in reasonable use, but we'll allow it 35 // and normalize to a null of the expected type rather than panicking 36 // below. 37 return cty.NullVal(schema.ImpliedType()) 38 } 39 40 vals := make(map[string]cty.Value) 41 for name := range schema.Attributes { 42 // No normalization for attributes, since them being type-conformant 43 // is all that we require. 44 vals[name] = val.GetAttr(name) 45 } 46 for name, blockS := range schema.BlockTypes { 47 lv := val.GetAttr(name) 48 49 // Legacy SDK never generates dynamically-typed attributes and so our 50 // normalization code doesn't deal with them, but we need to make sure 51 // we still pass them through properly so that we don't interfere with 52 // objects generated by other SDKs. 53 if ty := blockS.Block.ImpliedType(); ty.HasDynamicTypes() { 54 vals[name] = lv 55 continue 56 } 57 58 switch blockS.Nesting { 59 case configschema.NestingSingle, configschema.NestingGroup: 60 if lv.IsKnown() { 61 if lv.IsNull() && blockS.Nesting == configschema.NestingGroup { 62 vals[name] = blockS.EmptyValue() 63 } else { 64 vals[name] = normalizeObjectFromLegacySDK(lv, &blockS.Block) 65 } 66 } else { 67 vals[name] = unknownBlockStub(&blockS.Block) 68 } 69 case configschema.NestingList: 70 switch { 71 case !lv.IsKnown(): 72 vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)}) 73 case lv.IsNull() || lv.LengthInt() == 0: 74 vals[name] = cty.ListValEmpty(blockS.Block.ImpliedType()) 75 default: 76 subVals := make([]cty.Value, 0, lv.LengthInt()) 77 for it := lv.ElementIterator(); it.Next(); { 78 _, subVal := it.Element() 79 subVals = append(subVals, normalizeObjectFromLegacySDK(subVal, &blockS.Block)) 80 } 81 vals[name] = cty.ListVal(subVals) 82 } 83 case configschema.NestingSet: 84 switch { 85 case !lv.IsKnown(): 86 vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)}) 87 case lv.IsNull() || lv.LengthInt() == 0: 88 vals[name] = cty.SetValEmpty(blockS.Block.ImpliedType()) 89 default: 90 subVals := make([]cty.Value, 0, lv.LengthInt()) 91 for it := lv.ElementIterator(); it.Next(); { 92 _, subVal := it.Element() 93 subVals = append(subVals, normalizeObjectFromLegacySDK(subVal, &blockS.Block)) 94 } 95 vals[name] = cty.SetVal(subVals) 96 } 97 default: 98 // The legacy SDK doesn't support NestingMap, so we just assume 99 // maps are always okay. (If not, we would've detected and returned 100 // an error to the user before we got here.) 101 vals[name] = lv 102 } 103 } 104 return cty.ObjectVal(vals) 105 } 106 107 // unknownBlockStub constructs an object value that approximates an unknown 108 // block by producing a known block object with all of its leaf attribute 109 // values set to unknown. 110 // 111 // Blocks themselves cannot be unknown, so if the legacy SDK tries to return 112 // such a thing, we'll use this result instead. This convention mimics how 113 // the dynamic block feature deals with being asked to iterate over an unknown 114 // value, because our value-checking functions already accept this convention 115 // as a special case. 116 func unknownBlockStub(schema *configschema.Block) cty.Value { 117 vals := make(map[string]cty.Value) 118 for name, attrS := range schema.Attributes { 119 vals[name] = cty.UnknownVal(attrS.Type) 120 } 121 for name, blockS := range schema.BlockTypes { 122 switch blockS.Nesting { 123 case configschema.NestingSingle, configschema.NestingGroup: 124 vals[name] = unknownBlockStub(&blockS.Block) 125 case configschema.NestingList: 126 // In principle we may be expected to produce a tuple value here, 127 // if there are any dynamically-typed attributes in our nested block, 128 // but the legacy SDK doesn't support that, so we just assume it'll 129 // never be necessary to normalize those. (Incorrect usage in any 130 // other SDK would be caught and returned as an error before we 131 // get here.) 132 vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)}) 133 case configschema.NestingSet: 134 vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)}) 135 case configschema.NestingMap: 136 // A nesting map can never be unknown since we then wouldn't know 137 // what the keys are. (Legacy SDK doesn't support NestingMap anyway, 138 // so this should never arise.) 139 vals[name] = cty.MapValEmpty(blockS.Block.ImpliedType()) 140 } 141 } 142 return cty.ObjectVal(vals) 143 }