github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/hcl2shim/paths.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package hcl2shim 5 6 import ( 7 "fmt" 8 "reflect" 9 "strconv" 10 "strings" 11 12 "github.com/zclconf/go-cty/cty" 13 ) 14 15 // RequiresReplace takes a list of flatmapped paths from a 16 // InstanceDiff.Attributes along with the corresponding cty.Type, and returns 17 // the list of the cty.Paths that are flagged as causing the resource 18 // replacement (RequiresNew). 19 // This will filter out redundant paths, paths that refer to flatmapped indexes 20 // (e.g. "#", "%"), and will return any changes within a set as the path to the 21 // set itself. 22 func RequiresReplace(attrs []string, ty cty.Type) ([]cty.Path, error) { 23 var paths []cty.Path 24 25 for _, attr := range attrs { 26 p, err := requiresReplacePath(attr, ty) 27 if err != nil { 28 return nil, err 29 } 30 31 paths = append(paths, p) 32 } 33 34 // now trim off any trailing paths that aren't GetAttrSteps, since only an 35 // attribute itself can require replacement 36 paths = trimPaths(paths) 37 38 // There may be redundant paths due to set elements or index attributes 39 // Do some ugly n^2 filtering, but these are always fairly small sets. 40 for i := 0; i < len(paths)-1; i++ { 41 for j := i + 1; j < len(paths); j++ { 42 if reflect.DeepEqual(paths[i], paths[j]) { 43 // swap the tail and slice it off 44 paths[j], paths[len(paths)-1] = paths[len(paths)-1], paths[j] 45 paths = paths[:len(paths)-1] 46 j-- 47 } 48 } 49 } 50 51 return paths, nil 52 } 53 54 // trimPaths removes any trailing steps that aren't of type GetAttrSet, since 55 // only an attribute itself can require replacement 56 func trimPaths(paths []cty.Path) []cty.Path { 57 var trimmed []cty.Path 58 for _, path := range paths { 59 path = trimPath(path) 60 if len(path) > 0 { 61 trimmed = append(trimmed, path) 62 } 63 } 64 return trimmed 65 } 66 67 func trimPath(path cty.Path) cty.Path { 68 for len(path) > 0 { 69 _, isGetAttr := path[len(path)-1].(cty.GetAttrStep) 70 if isGetAttr { 71 break 72 } 73 path = path[:len(path)-1] 74 } 75 return path 76 } 77 78 // requiresReplacePath takes a key from a flatmap along with the cty.Type 79 // describing the structure, and returns the cty.Path that would be used to 80 // reference the nested value in the data structure. 81 // This is used specifically to record the RequiresReplace attributes from a 82 // ResourceInstanceDiff. 83 func requiresReplacePath(k string, ty cty.Type) (cty.Path, error) { 84 if k == "" { 85 return nil, nil 86 } 87 if !ty.IsObjectType() { 88 panic(fmt.Sprintf("requires replace path on non-object type: %#v", ty)) 89 } 90 91 path, err := pathFromFlatmapKeyObject(k, ty.AttributeTypes()) 92 if err != nil { 93 return path, fmt.Errorf("[%s] %s", k, err) 94 } 95 return path, nil 96 } 97 98 func pathSplit(p string) (string, string) { 99 parts := strings.SplitN(p, ".", 2) 100 head := parts[0] 101 rest := "" 102 if len(parts) > 1 { 103 rest = parts[1] 104 } 105 return head, rest 106 } 107 108 func pathFromFlatmapKeyObject(key string, atys map[string]cty.Type) (cty.Path, error) { 109 k, rest := pathSplit(key) 110 111 path := cty.Path{cty.GetAttrStep{Name: k}} 112 113 ty, ok := atys[k] 114 if !ok { 115 return path, fmt.Errorf("attribute %q not found", k) 116 } 117 118 if rest == "" { 119 return path, nil 120 } 121 122 p, err := pathFromFlatmapKeyValue(rest, ty) 123 if err != nil { 124 return path, err 125 } 126 127 return append(path, p...), nil 128 } 129 130 func pathFromFlatmapKeyValue(key string, ty cty.Type) (cty.Path, error) { 131 var path cty.Path 132 var err error 133 134 switch { 135 case ty.IsPrimitiveType(): 136 err = fmt.Errorf("invalid step %q with type %#v", key, ty) 137 case ty.IsObjectType(): 138 path, err = pathFromFlatmapKeyObject(key, ty.AttributeTypes()) 139 case ty.IsTupleType(): 140 path, err = pathFromFlatmapKeyTuple(key, ty.TupleElementTypes()) 141 case ty.IsMapType(): 142 path, err = pathFromFlatmapKeyMap(key, ty) 143 case ty.IsListType(): 144 path, err = pathFromFlatmapKeyList(key, ty) 145 case ty.IsSetType(): 146 path, err = pathFromFlatmapKeySet(key, ty) 147 default: 148 err = fmt.Errorf("unrecognized type: %s", ty.FriendlyName()) 149 } 150 151 if err != nil { 152 return path, err 153 } 154 155 return path, nil 156 } 157 158 func pathFromFlatmapKeyTuple(key string, etys []cty.Type) (cty.Path, error) { 159 var path cty.Path 160 var err error 161 162 k, rest := pathSplit(key) 163 164 // we don't need to convert the index keys to paths 165 if k == "#" { 166 return path, nil 167 } 168 169 idx, err := strconv.Atoi(k) 170 if err != nil { 171 return path, err 172 } 173 174 path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}} 175 176 if idx >= len(etys) { 177 return path, fmt.Errorf("index %s out of range in %#v", key, etys) 178 } 179 180 if rest == "" { 181 return path, nil 182 } 183 184 ty := etys[idx] 185 186 p, err := pathFromFlatmapKeyValue(rest, ty.ElementType()) 187 if err != nil { 188 return path, err 189 } 190 191 return append(path, p...), nil 192 } 193 194 func pathFromFlatmapKeyMap(key string, ty cty.Type) (cty.Path, error) { 195 var path cty.Path 196 var err error 197 198 k, rest := key, "" 199 if !ty.ElementType().IsPrimitiveType() { 200 k, rest = pathSplit(key) 201 } 202 203 // we don't need to convert the index keys to paths 204 if k == "%" { 205 return path, nil 206 } 207 208 path = cty.Path{cty.IndexStep{Key: cty.StringVal(k)}} 209 210 if rest == "" { 211 return path, nil 212 } 213 214 p, err := pathFromFlatmapKeyValue(rest, ty.ElementType()) 215 if err != nil { 216 return path, err 217 } 218 219 return append(path, p...), nil 220 } 221 222 func pathFromFlatmapKeyList(key string, ty cty.Type) (cty.Path, error) { 223 var path cty.Path 224 var err error 225 226 k, rest := pathSplit(key) 227 228 // we don't need to convert the index keys to paths 229 if key == "#" { 230 return path, nil 231 } 232 233 idx, err := strconv.Atoi(k) 234 if err != nil { 235 return path, err 236 } 237 238 path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}} 239 240 if rest == "" { 241 return path, nil 242 } 243 244 p, err := pathFromFlatmapKeyValue(rest, ty.ElementType()) 245 if err != nil { 246 return path, err 247 } 248 249 return append(path, p...), nil 250 } 251 252 func pathFromFlatmapKeySet(key string, ty cty.Type) (cty.Path, error) { 253 // once we hit a set, we can't return consistent paths, so just mark the 254 // set as a whole changed. 255 return nil, nil 256 } 257 258 // FlatmapKeyFromPath returns the flatmap equivalent of the given cty.Path for 259 // use in generating legacy style diffs. 260 func FlatmapKeyFromPath(path cty.Path) string { 261 var parts []string 262 263 for _, step := range path { 264 switch step := step.(type) { 265 case cty.GetAttrStep: 266 parts = append(parts, step.Name) 267 case cty.IndexStep: 268 switch ty := step.Key.Type(); { 269 case ty == cty.String: 270 parts = append(parts, step.Key.AsString()) 271 case ty == cty.Number: 272 i, _ := step.Key.AsBigFloat().Int64() 273 parts = append(parts, strconv.Itoa(int(i))) 274 } 275 } 276 } 277 278 return strings.Join(parts, ".") 279 }