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 }