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, ".") }