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