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  }