2018-05-22 02:39:26 +02:00
package funcs
import (
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"unicode/utf8"
2018-12-21 02:42:42 +01:00
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
2018-05-22 02:39:26 +02:00
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 ( )
2019-01-25 18:16:43 +01:00
src , err := readFileBytes ( baseDir , path )
2018-05-22 02:39:26 +02:00
if err != nil {
2019-01-25 18:16:43 +01:00
return cty . UnknownVal ( cty . String ) , err
2018-05-22 02:39:26 +02:00
}
switch {
case encBase64 :
enc := base64 . StdEncoding . EncodeToString ( src )
return cty . StringVal ( enc ) , nil
default :
if ! utf8 . Valid ( src ) {
2019-03-13 18:43:58 +01:00
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 )
2018-05-22 02:39:26 +02:00
}
return cty . StringVal ( string ( src ) ) , nil
}
} ,
} )
}
2018-12-21 02:42:42 +01:00
// MakeTemplateFileFunc constructs a function that takes a file path and
// an arbitrary object of named values and attempts to render the referenced
// file as a template using HCL template syntax.
//
// The template itself may recursively call other functions so a callback
// must be provided to get access to those functions. The template cannot,
// however, access any variables defined in the scope: it is restricted only to
// those variables provided in the second function argument, to ensure that all
// dependencies on other graph nodes can be seen before executing this function.
//
// As a special exception, a referenced template file may not recursively call
// the templatefile function, since that would risk the same file being
// included into itself indefinitely.
func MakeTemplateFileFunc ( baseDir string , funcsCb func ( ) map [ string ] function . Function ) function . Function {
params := [ ] function . Parameter {
{
Name : "path" ,
Type : cty . String ,
} ,
{
Name : "vars" ,
Type : cty . DynamicPseudoType ,
} ,
}
loadTmpl := func ( fn string ) ( hcl . Expression , error ) {
// We re-use File here to ensure the same filename interpretation
// as it does, along with its other safety checks.
tmplVal , err := File ( baseDir , cty . StringVal ( fn ) )
if err != nil {
return nil , err
}
expr , diags := hclsyntax . ParseTemplate ( [ ] byte ( tmplVal . AsString ( ) ) , fn , hcl . Pos { Line : 1 , Column : 1 } )
if diags . HasErrors ( ) {
return nil , diags
}
return expr , nil
}
renderTmpl := func ( expr hcl . Expression , varsVal cty . Value ) ( cty . Value , error ) {
if varsTy := varsVal . Type ( ) ; ! ( varsTy . IsMapType ( ) || varsTy . IsObjectType ( ) ) {
2019-03-20 00:07:04 +01:00
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
2018-12-21 02:42:42 +01:00
}
ctx := & hcl . EvalContext {
Variables : varsVal . AsValueMap ( ) ,
}
// We'll pre-check references in the template here so we can give a
// more specialized error message than HCL would by default, so it's
// clearer that this problem is coming from a templatefile call.
for _ , traversal := range expr . Variables ( ) {
root := traversal . RootName ( )
if _ , ok := ctx . Variables [ root ] ; ! ok {
2019-03-20 00:07:04 +01:00
return cty . DynamicVal , function . NewArgErrorf ( 1 , "vars map does not contain key %q, referenced at %s" , root , traversal [ 0 ] . SourceRange ( ) )
2018-12-21 02:42:42 +01:00
}
}
givenFuncs := funcsCb ( ) // this callback indirection is to avoid chicken/egg problems
funcs := make ( map [ string ] function . Function , len ( givenFuncs ) )
for name , fn := range givenFuncs {
if name == "templatefile" {
// We stub this one out to prevent recursive calls.
funcs [ name ] = function . New ( & function . Spec {
Params : params ,
Type : func ( args [ ] cty . Value ) ( cty . Type , error ) {
return cty . NilType , fmt . Errorf ( "cannot recursively call templatefile from inside templatefile call" )
} ,
} )
continue
}
funcs [ name ] = fn
}
ctx . Functions = funcs
val , diags := expr . Value ( ctx )
if diags . HasErrors ( ) {
return cty . DynamicVal , diags
}
return val , nil
}
return function . New ( & function . Spec {
Params : params ,
Type : func ( args [ ] cty . Value ) ( cty . Type , error ) {
if ! ( args [ 0 ] . IsKnown ( ) && args [ 1 ] . IsKnown ( ) ) {
return cty . DynamicPseudoType , nil
}
// We'll render our template now to see what result type it produces.
// A template consisting only of a single interpolation an potentially
// return any type.
expr , err := loadTmpl ( args [ 0 ] . AsString ( ) )
if err != nil {
return cty . DynamicPseudoType , err
}
// This is safe even if args[1] contains unknowns because the HCL
// template renderer itself knows how to short-circuit those.
val , err := renderTmpl ( expr , args [ 1 ] )
return val . Type ( ) , err
} ,
Impl : func ( args [ ] cty . Value , retType cty . Type ) ( cty . Value , error ) {
expr , err := loadTmpl ( args [ 0 ] . AsString ( ) )
if err != nil {
return cty . DynamicVal , err
}
return renderTmpl ( expr , args [ 1 ] )
} ,
} )
}
2018-10-11 12:40:09 +02:00
// 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 ( ) )
} ,
} )
}
2018-05-23 18:38:12 +02:00
// BasenameFunc constructs a function that takes a string containing a filesystem path
// and removes all except the last portion from it.
2018-05-23 00:04:49 +02:00
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
} ,
} )
2018-05-23 18:38:12 +02:00
// DirnameFunc constructs a function that takes a string containing a filesystem path
// and removes the last portion from it.
2018-05-23 00:24:50 +02:00
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
} ,
} )
2019-07-02 14:30:30 +02:00
// AbsPathFunc constructs a function that converts a filesystem path to an absolute path
var AbsPathFunc = 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 ) {
absPath , err := filepath . Abs ( args [ 0 ] . AsString ( ) )
return cty . StringVal ( filepath . ToSlash ( absPath ) ) , err
} ,
} )
2018-05-23 00:43:29 +02:00
// 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
} ,
} )
2019-01-25 18:16:43 +01:00
func readFileBytes ( baseDir , path string ) ( [ ] byte , error ) {
path , err := homedir . Expand ( path )
if err != nil {
return nil , 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 nil , fmt . Errorf ( "no file exists at %s" , path )
}
return nil , fmt . Errorf ( "failed to read %s" , path )
}
return src , nil
}
2018-05-22 02:39:26 +02:00
// 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 } )
2018-10-11 12:40:09 +02:00
}
// 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 } )
2018-05-22 02:39:26 +02:00
}
// 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 } )
}
2018-05-23 00:04:49 +02:00
// 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 } )
}
2018-05-23 00:24:50 +02:00
// 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 } )
}
2018-05-23 00:43:29 +02:00
// 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 } )
}