| package hcl2shim |
| |
| import ( |
| "fmt" |
| "reflect" |
| "strconv" |
| "strings" |
| |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // RequiresReplace takes a list of flatmapped paths from a |
| // InstanceDiff.Attributes along with the corresponding cty.Type, and returns |
| // the list of the cty.Paths that are flagged as causing the resource |
| // replacement (RequiresNew). |
| // This will filter out redundant paths, paths that refer to flatmapped indexes |
| // (e.g. "#", "%"), and will return any changes within a set as the path to the |
| // set itself. |
| func RequiresReplace(attrs []string, ty cty.Type) ([]cty.Path, error) { |
| var paths []cty.Path |
| |
| for _, attr := range attrs { |
| p, err := requiresReplacePath(attr, ty) |
| if err != nil { |
| return nil, err |
| } |
| |
| paths = append(paths, p) |
| } |
| |
| // now trim off any trailing paths that aren't GetAttrSteps, since only an |
| // attribute itself can require replacement |
| paths = trimPaths(paths) |
| |
| // There may be redundant paths due to set elements or index attributes |
| // Do some ugly n^2 filtering, but these are always fairly small sets. |
| for i := 0; i < len(paths)-1; i++ { |
| for j := i + 1; j < len(paths); j++ { |
| if reflect.DeepEqual(paths[i], paths[j]) { |
| // swap the tail and slice it off |
| paths[j], paths[len(paths)-1] = paths[len(paths)-1], paths[j] |
| paths = paths[:len(paths)-1] |
| j-- |
| } |
| } |
| } |
| |
| return paths, nil |
| } |
| |
| // trimPaths removes any trailing steps that aren't of type GetAttrSet, since |
| // only an attribute itself can require replacement |
| func trimPaths(paths []cty.Path) []cty.Path { |
| var trimmed []cty.Path |
| for _, path := range paths { |
| path = trimPath(path) |
| if len(path) > 0 { |
| trimmed = append(trimmed, path) |
| } |
| } |
| return trimmed |
| } |
| |
| func trimPath(path cty.Path) cty.Path { |
| for len(path) > 0 { |
| _, isGetAttr := path[len(path)-1].(cty.GetAttrStep) |
| if isGetAttr { |
| break |
| } |
| path = path[:len(path)-1] |
| } |
| return path |
| } |
| |
| // requiresReplacePath takes a key from a flatmap along with the cty.Type |
| // describing the structure, and returns the cty.Path that would be used to |
| // reference the nested value in the data structure. |
| // This is used specifically to record the RequiresReplace attributes from a |
| // ResourceInstanceDiff. |
| func requiresReplacePath(k string, ty cty.Type) (cty.Path, error) { |
| if k == "" { |
| return nil, nil |
| } |
| if !ty.IsObjectType() { |
| panic(fmt.Sprintf("requires replace path on non-object type: %#v", ty)) |
| } |
| |
| path, err := pathFromFlatmapKeyObject(k, ty.AttributeTypes()) |
| if err != nil { |
| return path, fmt.Errorf("[%s] %s", k, err) |
| } |
| return path, nil |
| } |
| |
| func pathSplit(p string) (string, string) { |
| parts := strings.SplitN(p, ".", 2) |
| head := parts[0] |
| rest := "" |
| if len(parts) > 1 { |
| rest = parts[1] |
| } |
| return head, rest |
| } |
| |
| func pathFromFlatmapKeyObject(key string, atys map[string]cty.Type) (cty.Path, error) { |
| k, rest := pathSplit(key) |
| |
| path := cty.Path{cty.GetAttrStep{Name: k}} |
| |
| ty, ok := atys[k] |
| if !ok { |
| return path, fmt.Errorf("attribute %q not found", k) |
| } |
| |
| if rest == "" { |
| return path, nil |
| } |
| |
| p, err := pathFromFlatmapKeyValue(rest, ty) |
| if err != nil { |
| return path, err |
| } |
| |
| return append(path, p...), nil |
| } |
| |
| func pathFromFlatmapKeyValue(key string, ty cty.Type) (cty.Path, error) { |
| var path cty.Path |
| var err error |
| |
| switch { |
| case ty.IsPrimitiveType(): |
| err = fmt.Errorf("invalid step %q with type %#v", key, ty) |
| case ty.IsObjectType(): |
| path, err = pathFromFlatmapKeyObject(key, ty.AttributeTypes()) |
| case ty.IsTupleType(): |
| path, err = pathFromFlatmapKeyTuple(key, ty.TupleElementTypes()) |
| case ty.IsMapType(): |
| path, err = pathFromFlatmapKeyMap(key, ty) |
| case ty.IsListType(): |
| path, err = pathFromFlatmapKeyList(key, ty) |
| case ty.IsSetType(): |
| path, err = pathFromFlatmapKeySet(key, ty) |
| default: |
| err = fmt.Errorf("unrecognized type: %s", ty.FriendlyName()) |
| } |
| |
| if err != nil { |
| return path, err |
| } |
| |
| return path, nil |
| } |
| |
| func pathFromFlatmapKeyTuple(key string, etys []cty.Type) (cty.Path, error) { |
| var path cty.Path |
| var err error |
| |
| k, rest := pathSplit(key) |
| |
| // we don't need to convert the index keys to paths |
| if k == "#" { |
| return path, nil |
| } |
| |
| idx, err := strconv.Atoi(k) |
| if err != nil { |
| return path, err |
| } |
| |
| path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}} |
| |
| if idx >= len(etys) { |
| return path, fmt.Errorf("index %s out of range in %#v", key, etys) |
| } |
| |
| if rest == "" { |
| return path, nil |
| } |
| |
| ty := etys[idx] |
| |
| p, err := pathFromFlatmapKeyValue(rest, ty.ElementType()) |
| if err != nil { |
| return path, err |
| } |
| |
| return append(path, p...), nil |
| } |
| |
| func pathFromFlatmapKeyMap(key string, ty cty.Type) (cty.Path, error) { |
| var path cty.Path |
| var err error |
| |
| k, rest := key, "" |
| if !ty.ElementType().IsPrimitiveType() { |
| k, rest = pathSplit(key) |
| } |
| |
| // we don't need to convert the index keys to paths |
| if k == "%" { |
| return path, nil |
| } |
| |
| path = cty.Path{cty.IndexStep{Key: cty.StringVal(k)}} |
| |
| if rest == "" { |
| return path, nil |
| } |
| |
| p, err := pathFromFlatmapKeyValue(rest, ty.ElementType()) |
| if err != nil { |
| return path, err |
| } |
| |
| return append(path, p...), nil |
| } |
| |
| func pathFromFlatmapKeyList(key string, ty cty.Type) (cty.Path, error) { |
| var path cty.Path |
| var err error |
| |
| k, rest := pathSplit(key) |
| |
| // we don't need to convert the index keys to paths |
| if key == "#" { |
| return path, nil |
| } |
| |
| idx, err := strconv.Atoi(k) |
| if err != nil { |
| return path, err |
| } |
| |
| path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}} |
| |
| if rest == "" { |
| return path, nil |
| } |
| |
| p, err := pathFromFlatmapKeyValue(rest, ty.ElementType()) |
| if err != nil { |
| return path, err |
| } |
| |
| return append(path, p...), nil |
| } |
| |
| func pathFromFlatmapKeySet(key string, ty cty.Type) (cty.Path, error) { |
| // once we hit a set, we can't return consistent paths, so just mark the |
| // set as a whole changed. |
| return nil, nil |
| } |
| |
| // FlatmapKeyFromPath returns the flatmap equivalent of the given cty.Path for |
| // use in generating legacy style diffs. |
| func FlatmapKeyFromPath(path cty.Path) string { |
| var parts []string |
| |
| for _, step := range path { |
| switch step := step.(type) { |
| case cty.GetAttrStep: |
| parts = append(parts, step.Name) |
| case cty.IndexStep: |
| switch ty := step.Key.Type(); { |
| case ty == cty.String: |
| parts = append(parts, step.Key.AsString()) |
| case ty == cty.Number: |
| i, _ := step.Key.AsBigFloat().Int64() |
| parts = append(parts, strconv.Itoa(int(i))) |
| } |
| } |
| } |
| |
| return strings.Join(parts, ".") |
| } |