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