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