github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/lang/funcs/filesystem.go (about)

     1  package funcs
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"unicode/utf8"
    10  
    11  	"github.com/bmatcuk/doublestar"
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/hashicorp/hcl/v2/hclsyntax"
    14  	homedir "github.com/mitchellh/go-homedir"
    15  	"github.com/zclconf/go-cty/cty"
    16  	"github.com/zclconf/go-cty/cty/function"
    17  )
    18  
    19  // MakeFileFunc constructs a function that takes a file path and returns the
    20  // contents of that file, either directly as a string (where valid UTF-8 is
    21  // required) or as a string containing base64 bytes.
    22  func MakeFileFunc(baseDir string, encBase64 bool) function.Function {
    23  	return function.New(&function.Spec{
    24  		Params: []function.Parameter{
    25  			{
    26  				Name: "path",
    27  				Type: cty.String,
    28  			},
    29  		},
    30  		Type: function.StaticReturnType(cty.String),
    31  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    32  			path := args[0].AsString()
    33  			src, err := readFileBytes(baseDir, path)
    34  			if err != nil {
    35  				return cty.UnknownVal(cty.String), err
    36  			}
    37  
    38  			switch {
    39  			case encBase64:
    40  				enc := base64.StdEncoding.EncodeToString(src)
    41  				return cty.StringVal(enc), nil
    42  			default:
    43  				if !utf8.Valid(src) {
    44  					return cty.UnknownVal(cty.String), fmt.Errorf("contents of %s are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead", path)
    45  				}
    46  				return cty.StringVal(string(src)), nil
    47  			}
    48  		},
    49  	})
    50  }
    51  
    52  // MakeTemplateFileFunc constructs a function that takes a file path and
    53  // an arbitrary object of named values and attempts to render the referenced
    54  // file as a template using HCL template syntax.
    55  //
    56  // The template itself may recursively call other functions so a callback
    57  // must be provided to get access to those functions. The template cannot,
    58  // however, access any variables defined in the scope: it is restricted only to
    59  // those variables provided in the second function argument, to ensure that all
    60  // dependencies on other graph nodes can be seen before executing this function.
    61  //
    62  // As a special exception, a referenced template file may not recursively call
    63  // the templatefile function, since that would risk the same file being
    64  // included into itself indefinitely.
    65  func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Function) function.Function {
    66  
    67  	params := []function.Parameter{
    68  		{
    69  			Name: "path",
    70  			Type: cty.String,
    71  		},
    72  		{
    73  			Name: "vars",
    74  			Type: cty.DynamicPseudoType,
    75  		},
    76  	}
    77  
    78  	loadTmpl := func(fn string) (hcl.Expression, error) {
    79  		// We re-use File here to ensure the same filename interpretation
    80  		// as it does, along with its other safety checks.
    81  		tmplVal, err := File(baseDir, cty.StringVal(fn))
    82  		if err != nil {
    83  			return nil, err
    84  		}
    85  
    86  		expr, diags := hclsyntax.ParseTemplate([]byte(tmplVal.AsString()), fn, hcl.Pos{Line: 1, Column: 1})
    87  		if diags.HasErrors() {
    88  			return nil, diags
    89  		}
    90  
    91  		return expr, nil
    92  	}
    93  
    94  	renderTmpl := func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) {
    95  		if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) {
    96  			return cty.DynamicVal, function.NewArgErrorf(1, "invalid vars value: must be a map") // or an object, but we don't strongly distinguish these most of the time
    97  		}
    98  
    99  		ctx := &hcl.EvalContext{
   100  			Variables: varsVal.AsValueMap(),
   101  		}
   102  
   103  		// We'll pre-check references in the template here so we can give a
   104  		// more specialized error message than HCL would by default, so it's
   105  		// clearer that this problem is coming from a templatefile call.
   106  		for _, traversal := range expr.Variables() {
   107  			root := traversal.RootName()
   108  			if _, ok := ctx.Variables[root]; !ok {
   109  				return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q, referenced at %s", root, traversal[0].SourceRange())
   110  			}
   111  		}
   112  
   113  		givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems
   114  		funcs := make(map[string]function.Function, len(givenFuncs))
   115  		for name, fn := range givenFuncs {
   116  			if name == "templatefile" {
   117  				// We stub this one out to prevent recursive calls.
   118  				funcs[name] = function.New(&function.Spec{
   119  					Params: params,
   120  					Type: func(args []cty.Value) (cty.Type, error) {
   121  						return cty.NilType, fmt.Errorf("cannot recursively call templatefile from inside templatefile call")
   122  					},
   123  				})
   124  				continue
   125  			}
   126  			funcs[name] = fn
   127  		}
   128  		ctx.Functions = funcs
   129  
   130  		val, diags := expr.Value(ctx)
   131  		if diags.HasErrors() {
   132  			return cty.DynamicVal, diags
   133  		}
   134  		return val, nil
   135  	}
   136  
   137  	return function.New(&function.Spec{
   138  		Params: params,
   139  		Type: func(args []cty.Value) (cty.Type, error) {
   140  			if !(args[0].IsKnown() && args[1].IsKnown()) {
   141  				return cty.DynamicPseudoType, nil
   142  			}
   143  
   144  			// We'll render our template now to see what result type it produces.
   145  			// A template consisting only of a single interpolation an potentially
   146  			// return any type.
   147  			expr, err := loadTmpl(args[0].AsString())
   148  			if err != nil {
   149  				return cty.DynamicPseudoType, err
   150  			}
   151  
   152  			// This is safe even if args[1] contains unknowns because the HCL
   153  			// template renderer itself knows how to short-circuit those.
   154  			val, err := renderTmpl(expr, args[1])
   155  			return val.Type(), err
   156  		},
   157  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   158  			expr, err := loadTmpl(args[0].AsString())
   159  			if err != nil {
   160  				return cty.DynamicVal, err
   161  			}
   162  			return renderTmpl(expr, args[1])
   163  		},
   164  	})
   165  
   166  }
   167  
   168  // MakeFileExistsFunc constructs a function that takes a path
   169  // and determines whether a file exists at that path
   170  func MakeFileExistsFunc(baseDir string) function.Function {
   171  	return function.New(&function.Spec{
   172  		Params: []function.Parameter{
   173  			{
   174  				Name: "path",
   175  				Type: cty.String,
   176  			},
   177  		},
   178  		Type: function.StaticReturnType(cty.Bool),
   179  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   180  			path := args[0].AsString()
   181  			path, err := homedir.Expand(path)
   182  			if err != nil {
   183  				return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to expand ~: %s", err)
   184  			}
   185  
   186  			if !filepath.IsAbs(path) {
   187  				path = filepath.Join(baseDir, path)
   188  			}
   189  
   190  			// Ensure that the path is canonical for the host OS
   191  			path = filepath.Clean(path)
   192  
   193  			fi, err := os.Stat(path)
   194  			if err != nil {
   195  				if os.IsNotExist(err) {
   196  					return cty.False, nil
   197  				}
   198  				return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to stat %s", path)
   199  			}
   200  
   201  			if fi.Mode().IsRegular() {
   202  				return cty.True, nil
   203  			}
   204  
   205  			return cty.False, fmt.Errorf("%s is not a regular file, but %q",
   206  				path, fi.Mode().String())
   207  		},
   208  	})
   209  }
   210  
   211  // MakeFileSetFunc constructs a function that takes a glob pattern
   212  // and enumerates a file set from that pattern
   213  func MakeFileSetFunc(baseDir string) function.Function {
   214  	return function.New(&function.Spec{
   215  		Params: []function.Parameter{
   216  			{
   217  				Name: "path",
   218  				Type: cty.String,
   219  			},
   220  			{
   221  				Name: "pattern",
   222  				Type: cty.String,
   223  			},
   224  		},
   225  		Type: function.StaticReturnType(cty.Set(cty.String)),
   226  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   227  			path := args[0].AsString()
   228  			pattern := args[1].AsString()
   229  
   230  			if !filepath.IsAbs(path) {
   231  				path = filepath.Join(baseDir, path)
   232  			}
   233  
   234  			// Join the path to the glob pattern, while ensuring the full
   235  			// pattern is canonical for the host OS. The joined path is
   236  			// automatically cleaned during this operation.
   237  			pattern = filepath.Join(path, pattern)
   238  
   239  			matches, err := doublestar.Glob(pattern)
   240  			if err != nil {
   241  				return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err)
   242  			}
   243  
   244  			var matchVals []cty.Value
   245  			for _, match := range matches {
   246  				fi, err := os.Stat(match)
   247  
   248  				if err != nil {
   249  					return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to stat (%s): %s", match, err)
   250  				}
   251  
   252  				if !fi.Mode().IsRegular() {
   253  					continue
   254  				}
   255  
   256  				// Remove the path and file separator from matches.
   257  				match, err = filepath.Rel(path, match)
   258  
   259  				if err != nil {
   260  					return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to trim path of match (%s): %s", match, err)
   261  				}
   262  
   263  				// Replace any remaining file separators with forward slash (/)
   264  				// separators for cross-system compatibility.
   265  				match = filepath.ToSlash(match)
   266  
   267  				matchVals = append(matchVals, cty.StringVal(match))
   268  			}
   269  
   270  			if len(matchVals) == 0 {
   271  				return cty.SetValEmpty(cty.String), nil
   272  			}
   273  
   274  			return cty.SetVal(matchVals), nil
   275  		},
   276  	})
   277  }
   278  
   279  // BasenameFunc constructs a function that takes a string containing a filesystem path
   280  // and removes all except the last portion from it.
   281  var BasenameFunc = function.New(&function.Spec{
   282  	Params: []function.Parameter{
   283  		{
   284  			Name: "path",
   285  			Type: cty.String,
   286  		},
   287  	},
   288  	Type: function.StaticReturnType(cty.String),
   289  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   290  		return cty.StringVal(filepath.Base(args[0].AsString())), nil
   291  	},
   292  })
   293  
   294  // DirnameFunc constructs a function that takes a string containing a filesystem path
   295  // and removes the last portion from it.
   296  var DirnameFunc = function.New(&function.Spec{
   297  	Params: []function.Parameter{
   298  		{
   299  			Name: "path",
   300  			Type: cty.String,
   301  		},
   302  	},
   303  	Type: function.StaticReturnType(cty.String),
   304  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   305  		return cty.StringVal(filepath.Dir(args[0].AsString())), nil
   306  	},
   307  })
   308  
   309  // AbsPathFunc constructs a function that converts a filesystem path to an absolute path
   310  var AbsPathFunc = function.New(&function.Spec{
   311  	Params: []function.Parameter{
   312  		{
   313  			Name: "path",
   314  			Type: cty.String,
   315  		},
   316  	},
   317  	Type: function.StaticReturnType(cty.String),
   318  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   319  		absPath, err := filepath.Abs(args[0].AsString())
   320  		return cty.StringVal(filepath.ToSlash(absPath)), err
   321  	},
   322  })
   323  
   324  // PathExpandFunc constructs a function that expands a leading ~ character to the current user's home directory.
   325  var PathExpandFunc = function.New(&function.Spec{
   326  	Params: []function.Parameter{
   327  		{
   328  			Name: "path",
   329  			Type: cty.String,
   330  		},
   331  	},
   332  	Type: function.StaticReturnType(cty.String),
   333  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   334  
   335  		homePath, err := homedir.Expand(args[0].AsString())
   336  		return cty.StringVal(homePath), err
   337  	},
   338  })
   339  
   340  func readFileBytes(baseDir, path string) ([]byte, error) {
   341  	path, err := homedir.Expand(path)
   342  	if err != nil {
   343  		return nil, fmt.Errorf("failed to expand ~: %s", err)
   344  	}
   345  
   346  	if !filepath.IsAbs(path) {
   347  		path = filepath.Join(baseDir, path)
   348  	}
   349  
   350  	// Ensure that the path is canonical for the host OS
   351  	path = filepath.Clean(path)
   352  
   353  	src, err := ioutil.ReadFile(path)
   354  	if err != nil {
   355  		// ReadFile does not return Terraform-user-friendly error
   356  		// messages, so we'll provide our own.
   357  		if os.IsNotExist(err) {
   358  			return nil, fmt.Errorf("no file exists at %s", path)
   359  		}
   360  		return nil, fmt.Errorf("failed to read %s", path)
   361  	}
   362  
   363  	return src, nil
   364  }
   365  
   366  // File reads the contents of the file at the given path.
   367  //
   368  // The file must contain valid UTF-8 bytes, or this function will return an error.
   369  //
   370  // The underlying function implementation works relative to a particular base
   371  // directory, so this wrapper takes a base directory string and uses it to
   372  // construct the underlying function before calling it.
   373  func File(baseDir string, path cty.Value) (cty.Value, error) {
   374  	fn := MakeFileFunc(baseDir, false)
   375  	return fn.Call([]cty.Value{path})
   376  }
   377  
   378  // FileExists determines whether a file exists at the given path.
   379  //
   380  // The underlying function implementation works relative to a particular base
   381  // directory, so this wrapper takes a base directory string and uses it to
   382  // construct the underlying function before calling it.
   383  func FileExists(baseDir string, path cty.Value) (cty.Value, error) {
   384  	fn := MakeFileExistsFunc(baseDir)
   385  	return fn.Call([]cty.Value{path})
   386  }
   387  
   388  // FileSet enumerates a set of files given a glob pattern
   389  //
   390  // The underlying function implementation works relative to a particular base
   391  // directory, so this wrapper takes a base directory string and uses it to
   392  // construct the underlying function before calling it.
   393  func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) {
   394  	fn := MakeFileSetFunc(baseDir)
   395  	return fn.Call([]cty.Value{path, pattern})
   396  }
   397  
   398  // FileBase64 reads the contents of the file at the given path.
   399  //
   400  // The bytes from the file are encoded as base64 before returning.
   401  //
   402  // The underlying function implementation works relative to a particular base
   403  // directory, so this wrapper takes a base directory string and uses it to
   404  // construct the underlying function before calling it.
   405  func FileBase64(baseDir string, path cty.Value) (cty.Value, error) {
   406  	fn := MakeFileFunc(baseDir, true)
   407  	return fn.Call([]cty.Value{path})
   408  }
   409  
   410  // Basename takes a string containing a filesystem path and removes all except the last portion from it.
   411  //
   412  // The underlying function implementation works only with the path string and does not access the filesystem itself.
   413  // It is therefore unable to take into account filesystem features such as symlinks.
   414  //
   415  // If the path is empty then the result is ".", representing the current working directory.
   416  func Basename(path cty.Value) (cty.Value, error) {
   417  	return BasenameFunc.Call([]cty.Value{path})
   418  }
   419  
   420  // Dirname takes a string containing a filesystem path and removes the last portion from it.
   421  //
   422  // The underlying function implementation works only with the path string and does not access the filesystem itself.
   423  // It is therefore unable to take into account filesystem features such as symlinks.
   424  //
   425  // If the path is empty then the result is ".", representing the current working directory.
   426  func Dirname(path cty.Value) (cty.Value, error) {
   427  	return DirnameFunc.Call([]cty.Value{path})
   428  }
   429  
   430  // Pathexpand takes a string that might begin with a `~` segment, and if so it replaces that segment with
   431  // the current user's home directory path.
   432  //
   433  // The underlying function implementation works only with the path string and does not access the filesystem itself.
   434  // It is therefore unable to take into account filesystem features such as symlinks.
   435  //
   436  // If the leading segment in the path is not `~` then the given path is returned unmodified.
   437  func Pathexpand(path cty.Value) (cty.Value, error) {
   438  	return PathExpandFunc.Call([]cty.Value{path})
   439  }