github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/configupgrade/module_sources.go (about)

     1  package configupgrade
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/zclconf/go-cty/cty"
    10  
    11  	"github.com/hashicorp/terraform/configs"
    12  	"github.com/hashicorp/terraform/tfdiags"
    13  
    14  	"github.com/hashicorp/hcl/v2"
    15  	hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
    16  
    17  	version "github.com/hashicorp/go-version"
    18  )
    19  
    20  type ModuleSources map[string][]byte
    21  
    22  // LoadModule looks for Terraform configuration files in the given directory
    23  // and loads each of them into memory as source code, in preparation for
    24  // further analysis and conversion.
    25  //
    26  // At this stage the files are not parsed at all. Instead, we just read the
    27  // raw bytes from the file so that they can be passed into a parser in a
    28  // separate step.
    29  //
    30  // If the given directory or any of the files cannot be read, an error is
    31  // returned. It is not safe to proceed with processing in that case because
    32  // we cannot "see" all of the source code for the configuration.
    33  func LoadModule(dir string) (ModuleSources, error) {
    34  	entries, err := ioutil.ReadDir(dir)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	ret := make(ModuleSources)
    40  	for _, entry := range entries {
    41  		name := entry.Name()
    42  		if entry.IsDir() {
    43  			continue
    44  		}
    45  		if configs.IsIgnoredFile(name) {
    46  			continue
    47  		}
    48  		ext := fileExt(name)
    49  		if ext == "" {
    50  			continue
    51  		}
    52  
    53  		fullPath := filepath.Join(dir, name)
    54  		src, err := ioutil.ReadFile(fullPath)
    55  		if err != nil {
    56  			return nil, err
    57  		}
    58  
    59  		ret[name] = src
    60  	}
    61  
    62  	return ret, nil
    63  }
    64  
    65  // UnusedFilename finds a filename that isn't already used by a file in
    66  // the receiving sources and returns it.
    67  //
    68  // The given "proposed" name is returned verbatim if it isn't already used.
    69  // Otherwise, the function will try appending incrementing integers to the
    70  // proposed name until an unused name is found. Callers should propose names
    71  // that they do not expect to already be in use so that numeric suffixes are
    72  // only used in rare cases.
    73  //
    74  // The proposed name must end in either ".tf" or ".tf.json" because a
    75  // ModuleSources only has visibility into such files. This function will
    76  // panic if given a file whose name does not end with one of these
    77  // extensions.
    78  //
    79  // A ModuleSources only works on one directory at a time, so the proposed
    80  // name must not contain any directory separator characters.
    81  func (ms ModuleSources) UnusedFilename(proposed string) string {
    82  	ext := fileExt(proposed)
    83  	if ext == "" {
    84  		panic(fmt.Errorf("method UnusedFilename used with invalid proposal %q", proposed))
    85  	}
    86  
    87  	if _, exists := ms[proposed]; !exists {
    88  		return proposed
    89  	}
    90  
    91  	base := proposed[:len(proposed)-len(ext)]
    92  	for i := 1; ; i++ {
    93  		try := fmt.Sprintf("%s-%d%s", base, i, ext)
    94  		if _, exists := ms[try]; !exists {
    95  			return try
    96  		}
    97  	}
    98  }
    99  
   100  // MaybeAlreadyUpgraded is a heuristic to see if a given module may have
   101  // already been upgraded by this package.
   102  //
   103  // The heuristic used is to look for a Terraform Core version constraint in
   104  // any of the given sources that seems to be requiring a version greater than
   105  // or equal to v0.12.0. If true is returned then the source range of the found
   106  // version constraint is returned in case the caller wishes to present it to
   107  // the user as context for a warning message. The returned range is not
   108  // meaningful if false is returned.
   109  func (ms ModuleSources) MaybeAlreadyUpgraded() (bool, tfdiags.SourceRange) {
   110  	for name, src := range ms {
   111  		f, diags := hcl2syntax.ParseConfig(src, name, hcl.Pos{Line: 1, Column: 1})
   112  		if diags.HasErrors() {
   113  			// If we can't parse at all then that's a reasonable signal that
   114  			// we _haven't_ been upgraded yet, but we'll continue checking
   115  			// other files anyway.
   116  			continue
   117  		}
   118  
   119  		content, _, diags := f.Body.PartialContent(&hcl.BodySchema{
   120  			Blocks: []hcl.BlockHeaderSchema{
   121  				{
   122  					Type: "terraform",
   123  				},
   124  			},
   125  		})
   126  		if diags.HasErrors() {
   127  			// Suggests that the file has an invalid "terraform" block, such
   128  			// as one with labels.
   129  			continue
   130  		}
   131  
   132  		for _, block := range content.Blocks {
   133  			content, _, diags := block.Body.PartialContent(&hcl.BodySchema{
   134  				Attributes: []hcl.AttributeSchema{
   135  					{
   136  						Name: "required_version",
   137  					},
   138  				},
   139  			})
   140  			if diags.HasErrors() {
   141  				continue
   142  			}
   143  
   144  			attr, present := content.Attributes["required_version"]
   145  			if !present {
   146  				continue
   147  			}
   148  
   149  			constraintVal, diags := attr.Expr.Value(nil)
   150  			if diags.HasErrors() {
   151  				continue
   152  			}
   153  			if constraintVal.Type() != cty.String || constraintVal.IsNull() {
   154  				continue
   155  			}
   156  
   157  			constraints, err := version.NewConstraint(constraintVal.AsString())
   158  			if err != nil {
   159  				continue
   160  			}
   161  
   162  			// The go-version package doesn't actually let us see the details
   163  			// of the parsed constraints here, so we now need a bit of an
   164  			// abstraction inversion to decide if any of the given constraints
   165  			// match our heuristic. However, we do at least get to benefit
   166  			// from go-version's ability to extract multiple constraints from
   167  			// a single string and the fact that it's already validated each
   168  			// constraint to match its expected pattern.
   169  		Constraints:
   170  			for _, constraint := range constraints {
   171  				str := strings.TrimSpace(constraint.String())
   172  				// Want to match >, >= and ~> here.
   173  				if !(strings.HasPrefix(str, ">") || strings.HasPrefix(str, "~>")) {
   174  					continue
   175  				}
   176  
   177  				// Try to find something in this string that'll parse as a version.
   178  				for i := 1; i < len(str); i++ {
   179  					candidate := str[i:]
   180  					v, err := version.NewVersion(candidate)
   181  					if err != nil {
   182  						continue
   183  					}
   184  
   185  					if v.Equal(firstVersionWithNewParser) || v.GreaterThan(firstVersionWithNewParser) {
   186  						// This constraint appears to be preventing the old
   187  						// parser from being used, so we suspect it was
   188  						// already upgraded.
   189  						return true, tfdiags.SourceRangeFromHCL(attr.Range)
   190  					}
   191  
   192  					// If we fall out here then we _did_ find something that
   193  					// parses as a version, so we'll stop and move on to the
   194  					// next constraint. (Otherwise we'll pass by 0.7.0 and find
   195  					// 7.0, which is also a valid version.)
   196  					continue Constraints
   197  				}
   198  			}
   199  		}
   200  	}
   201  	return false, tfdiags.SourceRange{}
   202  }
   203  
   204  var firstVersionWithNewParser = version.Must(version.NewVersion("0.12.0"))
   205  
   206  // fileExt returns the Terraform configuration extension of the given
   207  // path, or a blank string if it is not a recognized extension.
   208  func fileExt(path string) string {
   209  	if strings.HasSuffix(path, ".tf") {
   210  		return ".tf"
   211  	} else if strings.HasSuffix(path, ".tf.json") {
   212  		return ".tf.json"
   213  	} else {
   214  		return ""
   215  	}
   216  }