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 }