package funcs import ( "encoding/base64" "fmt" "io/ioutil" "os" "path/filepath" "unicode/utf8" homedir "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" ) // MakeFileFunc constructs a function that takes a file path and returns the // contents of that file, either directly as a string (where valid UTF-8 is // required) or as a string containing base64 bytes. func MakeFileFunc(baseDir string, encBase64 bool) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { Name: "path", Type: cty.String, }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { path := args[0].AsString() path, err := homedir.Expand(path) if err != nil { return cty.UnknownVal(cty.String), fmt.Errorf("failed to expand ~: %s", err) } if !filepath.IsAbs(path) { path = filepath.Join(baseDir, path) } // Ensure that the path is canonical for the host OS path = filepath.Clean(path) src, err := ioutil.ReadFile(path) if err != nil { // ReadFile does not return Terraform-user-friendly error // messages, so we'll provide our own. if os.IsNotExist(err) { return cty.UnknownVal(cty.String), fmt.Errorf("no file exists at %s", path) } return cty.UnknownVal(cty.String), fmt.Errorf("failed to read %s", path) } switch { case encBase64: enc := base64.StdEncoding.EncodeToString(src) return cty.StringVal(enc), nil default: if !utf8.Valid(src) { return cty.UnknownVal(cty.String), fmt.Errorf("contents of %s are not valid UTF-8; to read arbitrary bytes, use the filebase64 function instead", path) } return cty.StringVal(string(src)), nil } }, }) } // MakeFileExistsFunc constructs a function that takes a path // and determines whether a file exists at that path func MakeFileExistsFunc(baseDir string) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { Name: "path", Type: cty.String, }, }, Type: function.StaticReturnType(cty.Bool), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { path := args[0].AsString() path, err := homedir.Expand(path) if err != nil { return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to expand ~: %s", err) } if !filepath.IsAbs(path) { path = filepath.Join(baseDir, path) } // Ensure that the path is canonical for the host OS path = filepath.Clean(path) fi, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { return cty.False, nil } return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to stat %s", path) } if fi.Mode().IsRegular() { return cty.True, nil } return cty.False, fmt.Errorf("%s is not a regular file, but %q", path, fi.Mode().String()) }, }) } // BasenameFunc constructs a function that takes a string containing a filesystem path // and removes all except the last portion from it. var BasenameFunc = function.New(&function.Spec{ Params: []function.Parameter{ { Name: "path", Type: cty.String, }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(filepath.Base(args[0].AsString())), nil }, }) // DirnameFunc constructs a function that takes a string containing a filesystem path // and removes the last portion from it. var DirnameFunc = function.New(&function.Spec{ Params: []function.Parameter{ { Name: "path", Type: cty.String, }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(filepath.Dir(args[0].AsString())), nil }, }) // PathExpandFunc constructs a function that expands a leading ~ character to the current user's home directory. var PathExpandFunc = function.New(&function.Spec{ Params: []function.Parameter{ { Name: "path", Type: cty.String, }, }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { homePath, err := homedir.Expand(args[0].AsString()) return cty.StringVal(homePath), err }, }) // File reads the contents of the file at the given path. // // The file must contain valid UTF-8 bytes, or this function will return an error. // // The underlying function implementation works relative to a particular base // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func File(baseDir string, path cty.Value) (cty.Value, error) { fn := MakeFileFunc(baseDir, false) return fn.Call([]cty.Value{path}) } // FileExists determines whether a file exists at the given path. // // The underlying function implementation works relative to a particular base // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func FileExists(baseDir string, path cty.Value) (cty.Value, error) { fn := MakeFileExistsFunc(baseDir) return fn.Call([]cty.Value{path}) } // FileBase64 reads the contents of the file at the given path. // // The bytes from the file are encoded as base64 before returning. // // The underlying function implementation works relative to a particular base // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func FileBase64(baseDir string, path cty.Value) (cty.Value, error) { fn := MakeFileFunc(baseDir, true) return fn.Call([]cty.Value{path}) } // Basename takes a string containing a filesystem path and removes all except the last portion from it. // // The underlying function implementation works only with the path string and does not access the filesystem itself. // It is therefore unable to take into account filesystem features such as symlinks. // // If the path is empty then the result is ".", representing the current working directory. func Basename(path cty.Value) (cty.Value, error) { return BasenameFunc.Call([]cty.Value{path}) } // Dirname takes a string containing a filesystem path and removes the last portion from it. // // The underlying function implementation works only with the path string and does not access the filesystem itself. // It is therefore unable to take into account filesystem features such as symlinks. // // If the path is empty then the result is ".", representing the current working directory. func Dirname(path cty.Value) (cty.Value, error) { return DirnameFunc.Call([]cty.Value{path}) } // Pathexpand takes a string that might begin with a `~` segment, and if so it replaces that segment with // the current user's home directory path. // // The underlying function implementation works only with the path string and does not access the filesystem itself. // It is therefore unable to take into account filesystem features such as symlinks. // // If the leading segment in the path is not `~` then the given path is returned unmodified. func Pathexpand(path cty.Value) (cty.Value, error) { return PathExpandFunc.Call([]cty.Value{path}) }