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