Merge pull request #19086 from hashicorp/f-file-exists-func

lang: Add file_exists function
This commit is contained in:
Radek Simko 2018-10-18 06:25:56 +01:00 committed by GitHub
commit d6f7577955
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 140 additions and 0 deletions

View File

@ -63,6 +63,49 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function {
}) })
} }
// 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 // BasenameFunc constructs a function that takes a string containing a filesystem path
// and removes all except the last portion from it. // and removes all except the last portion from it.
var BasenameFunc = function.New(&function.Spec{ var BasenameFunc = function.New(&function.Spec{
@ -121,6 +164,16 @@ func File(baseDir string, path cty.Value) (cty.Value, error) {
return fn.Call([]cty.Value{path}) 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. // FileBase64 reads the contents of the file at the given path.
// //
// The bytes from the file are encoded as base64 before returning. // The bytes from the file are encoded as base64 before returning.

View File

@ -52,6 +52,49 @@ func TestFile(t *testing.T) {
} }
} }
func TestFileExists(t *testing.T) {
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/hello.txt"),
cty.BoolVal(true),
false,
},
{
cty.StringVal(""), // empty path
cty.BoolVal(false),
true,
},
{
cty.StringVal("testdata/missing"),
cty.BoolVal(false),
false, // no file exists
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("FileExists(\".\", %#v)", test.Path), func(t *testing.T) {
got, err := FileExists(".", test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestFileBase64(t *testing.T) { func TestFileBase64(t *testing.T) {
tests := []struct { tests := []struct {
Path cty.Value Path cty.Value

View File

@ -53,6 +53,7 @@ func (s *Scope) Functions() map[string]function.Function {
"element": funcs.ElementFunc, "element": funcs.ElementFunc,
"chunklist": funcs.ChunklistFunc, "chunklist": funcs.ChunklistFunc,
"file": funcs.MakeFileFunc(s.BaseDir, false), "file": funcs.MakeFileFunc(s.BaseDir, false),
"fileexists": funcs.MakeFileExistsFunc(s.BaseDir),
"filebase64": funcs.MakeFileFunc(s.BaseDir, true), "filebase64": funcs.MakeFileFunc(s.BaseDir, true),
"flatten": funcs.FlattenFunc, "flatten": funcs.FlattenFunc,
"floor": funcs.FloorFunc, "floor": funcs.FloorFunc,

View File

@ -42,3 +42,5 @@ Hello World
* [`filebase64`](./filebase64.html) also reads the contents of a given file, * [`filebase64`](./filebase64.html) also reads the contents of a given file,
but returns the raw bytes in that file Base64-encoded, rather than but returns the raw bytes in that file Base64-encoded, rather than
interpreting the contents as UTF-8 text. interpreting the contents as UTF-8 text.
* [`fileexists`](./fileexists.html) determines whether a file exists
at a given path.

View File

@ -0,0 +1,37 @@
---
layout: "functions"
page_title: "fileexists function"
sidebar_current: "docs-funcs-file-file-exists"
description: |-
The fileexists function determines whether a file exists at a given path.
---
# `fileexists` Function
`fileexists` determines whether a file exists at a given path.
```hcl
fileexists(path)
```
Functions are evaluated during configuration parsing rather than at apply time,
so this function can only be used with files that are already present on disk
before Terraform takes any actions.
This function works only with regular files. If used with a directory, FIFO,
or other special mode, it will return an error.
## Examples
```
> fileexists("${path.module}/hello.txt")
true
```
```hcl
fileexists("custom-section.sh") ? file("custom-section.sh") : local.default_content
```
## Related Functions
* [`file`](./file.html) reads the contents of a file at a given path

View File

@ -249,6 +249,10 @@
<a href="/docs/configuration/functions/file.html">file</a> <a href="/docs/configuration/functions/file.html">file</a>
</li> </li>
<li<%= sidebar_current("docs-funcs-file-file-exists") %>>
<a href="/docs/configuration/functions/fileexists.html">fileexists</a>
</li>
<li<%= sidebar_current("docs-funcs-file-filebase64") %>> <li<%= sidebar_current("docs-funcs-file-filebase64") %>>
<a href="/docs/configuration/functions/filebase64.html">filebase64</a> <a href="/docs/configuration/functions/filebase64.html">filebase64</a>
</li> </li>