github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/field_reader_diff.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 schema 7 8 import ( 9 "fmt" 10 "strings" 11 12 "github.com/go-viper/mapstructure/v2" 13 "github.com/opentofu/opentofu/internal/legacy/tofu" 14 ) 15 16 // DiffFieldReader reads fields out of a diff structures. 17 // 18 // It also requires access to a Reader that reads fields from the structure 19 // that the diff was derived from. This is usually the state. This is required 20 // because a diff on its own doesn't have complete data about full objects 21 // such as maps. 22 // 23 // The Source MUST be the data that the diff was derived from. If it isn't, 24 // the behavior of this struct is undefined. 25 // 26 // Reading fields from a DiffFieldReader is identical to reading from 27 // Source except the diff will be applied to the end result. 28 // 29 // The "Exists" field on the result will be set to true if the complete 30 // field exists whether its from the source, diff, or a combination of both. 31 // It cannot be determined whether a retrieved value is composed of 32 // diff elements. 33 type DiffFieldReader struct { 34 Diff *tofu.InstanceDiff 35 Source FieldReader 36 Schema map[string]*Schema 37 38 // cache for memoizing ReadField calls. 39 cache map[string]cachedFieldReadResult 40 } 41 42 type cachedFieldReadResult struct { 43 val FieldReadResult 44 err error 45 } 46 47 func (r *DiffFieldReader) ReadField(address []string) (FieldReadResult, error) { 48 if r.cache == nil { 49 r.cache = make(map[string]cachedFieldReadResult) 50 } 51 52 // Create the cache key by joining around a value that isn't a valid part 53 // of an address. This assumes that the Source and Schema are not changed 54 // for the life of this DiffFieldReader. 55 cacheKey := strings.Join(address, "|") 56 if cached, ok := r.cache[cacheKey]; ok { 57 return cached.val, cached.err 58 } 59 60 schemaList := addrToSchema(address, r.Schema) 61 if len(schemaList) == 0 { 62 r.cache[cacheKey] = cachedFieldReadResult{} 63 return FieldReadResult{}, nil 64 } 65 66 var res FieldReadResult 67 var err error 68 69 schema := schemaList[len(schemaList)-1] 70 switch schema.Type { 71 case TypeBool, TypeInt, TypeFloat, TypeString: 72 res, err = r.readPrimitive(address, schema) 73 case TypeList: 74 res, err = readListField(r, address, schema) 75 case TypeMap: 76 res, err = r.readMap(address, schema) 77 case TypeSet: 78 res, err = r.readSet(address, schema) 79 case typeObject: 80 res, err = readObjectField(r, address, schema.Elem.(map[string]*Schema)) 81 default: 82 panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) 83 } 84 85 r.cache[cacheKey] = cachedFieldReadResult{ 86 val: res, 87 err: err, 88 } 89 return res, err 90 } 91 92 func (r *DiffFieldReader) readMap( 93 address []string, schema *Schema) (FieldReadResult, error) { 94 result := make(map[string]interface{}) 95 resultSet := false 96 97 // First read the map from the underlying source 98 source, err := r.Source.ReadField(address) 99 if err != nil { 100 return FieldReadResult{}, err 101 } 102 if source.Exists { 103 // readMap may return a nil value, or an unknown value placeholder in 104 // some cases, causing the type assertion to panic if we don't assign the ok value 105 result, _ = source.Value.(map[string]interface{}) 106 resultSet = true 107 } 108 109 // Next, read all the elements we have in our diff, and apply 110 // the diff to our result. 111 prefix := strings.Join(address, ".") + "." 112 for k, v := range r.Diff.Attributes { 113 if !strings.HasPrefix(k, prefix) { 114 continue 115 } 116 if strings.HasPrefix(k, prefix+"%") { 117 // Ignore the count field 118 continue 119 } 120 121 resultSet = true 122 123 k = k[len(prefix):] 124 if v.NewRemoved { 125 delete(result, k) 126 continue 127 } 128 129 result[k] = v.New 130 } 131 132 key := address[len(address)-1] 133 err = mapValuesToPrimitive(key, result, schema) 134 if err != nil { 135 return FieldReadResult{}, nil 136 } 137 138 var resultVal interface{} 139 if resultSet { 140 resultVal = result 141 } 142 143 return FieldReadResult{ 144 Value: resultVal, 145 Exists: resultSet, 146 }, nil 147 } 148 149 func (r *DiffFieldReader) readPrimitive( 150 address []string, schema *Schema) (FieldReadResult, error) { 151 result, err := r.Source.ReadField(address) 152 if err != nil { 153 return FieldReadResult{}, err 154 } 155 156 attrD, ok := r.Diff.Attributes[strings.Join(address, ".")] 157 if !ok { 158 return result, nil 159 } 160 161 var resultVal string 162 if !attrD.NewComputed { 163 resultVal = attrD.New 164 if attrD.NewExtra != nil { 165 result.ValueProcessed = resultVal 166 if err := mapstructure.WeakDecode(attrD.NewExtra, &resultVal); err != nil { 167 return FieldReadResult{}, err 168 } 169 } 170 } 171 172 result.Computed = attrD.NewComputed 173 result.Exists = true 174 result.Value, err = stringToPrimitive(resultVal, false, schema) 175 if err != nil { 176 return FieldReadResult{}, err 177 } 178 179 return result, nil 180 } 181 182 func (r *DiffFieldReader) readSet( 183 address []string, schema *Schema) (FieldReadResult, error) { 184 // copy address to ensure we don't modify the argument 185 address = append([]string(nil), address...) 186 187 prefix := strings.Join(address, ".") + "." 188 189 // Create the set that will be our result 190 set := schema.ZeroValue().(*Set) 191 192 // Go through the map and find all the set items 193 for k, d := range r.Diff.Attributes { 194 if d.NewRemoved { 195 // If the field is removed, we always ignore it 196 continue 197 } 198 if !strings.HasPrefix(k, prefix) { 199 continue 200 } 201 if strings.HasSuffix(k, "#") { 202 // Ignore any count field 203 continue 204 } 205 206 // Split the key, since it might be a sub-object like "idx.field" 207 parts := strings.Split(k[len(prefix):], ".") 208 idx := parts[0] 209 210 raw, err := r.ReadField(append(address, idx)) 211 if err != nil { 212 return FieldReadResult{}, err 213 } 214 if !raw.Exists { 215 // This shouldn't happen because we just verified it does exist 216 panic("missing field in set: " + k + "." + idx) 217 } 218 219 set.Add(raw.Value) 220 } 221 222 // Determine if the set "exists". It exists if there are items or if 223 // the diff explicitly wanted it empty. 224 exists := set.Len() > 0 225 if !exists { 226 // We could check if the diff value is "0" here but I think the 227 // existence of "#" on its own is enough to show it existed. This 228 // protects us in the future from the zero value changing from 229 // "0" to "" breaking us (if that were to happen). 230 if _, ok := r.Diff.Attributes[prefix+"#"]; ok { 231 exists = true 232 } 233 } 234 235 if !exists { 236 result, err := r.Source.ReadField(address) 237 if err != nil { 238 return FieldReadResult{}, err 239 } 240 if result.Exists { 241 return result, nil 242 } 243 } 244 245 return FieldReadResult{ 246 Value: set, 247 Exists: exists, 248 }, nil 249 }