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