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 }