2015-05-02 01:59:16 +02:00
package template
import (
"crypto/sha256"
"encoding/hex"
"fmt"
2015-06-17 20:58:01 +02:00
"log"
2015-05-21 22:01:28 +02:00
"os"
"path/filepath"
2015-05-02 01:59:16 +02:00
2016-01-31 08:45:15 +01:00
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
2015-05-02 01:59:16 +02:00
"github.com/hashicorp/terraform/config"
2015-11-13 18:07:02 +01:00
"github.com/hashicorp/terraform/helper/pathorcontents"
2015-05-02 01:59:16 +02:00
"github.com/hashicorp/terraform/helper/schema"
)
2015-10-06 01:08:33 +02:00
func resourceFile ( ) * schema . Resource {
2015-05-02 01:59:16 +02:00
return & schema . Resource {
2015-10-06 01:08:33 +02:00
Create : resourceFileCreate ,
Delete : resourceFileDelete ,
Exists : resourceFileExists ,
Read : resourceFileRead ,
2015-05-02 01:59:16 +02:00
Schema : map [ string ] * schema . Schema {
2015-11-13 18:07:02 +01:00
"template" : & schema . Schema {
Type : schema . TypeString ,
Optional : true ,
Description : "Contents of the template" ,
ForceNew : true ,
ConflictsWith : [ ] string { "filename" } ,
2016-03-10 19:34:56 +01:00
ValidateFunc : validateTemplateAttribute ,
2015-11-13 18:07:02 +01:00
} ,
2015-05-02 01:59:16 +02:00
"filename" : & schema . Schema {
Type : schema . TypeString ,
2015-11-13 18:07:02 +01:00
Optional : true ,
2015-05-02 01:59:16 +02:00
Description : "file to read template from" ,
2015-05-09 00:46:37 +02:00
ForceNew : true ,
2015-05-21 22:01:28 +02:00
// Make a "best effort" attempt to relativize the file path.
StateFunc : func ( v interface { } ) string {
2015-11-13 18:07:02 +01:00
if v == nil || v . ( string ) == "" {
return ""
}
2015-05-21 22:01:28 +02:00
pwd , err := os . Getwd ( )
if err != nil {
return v . ( string )
}
rel , err := filepath . Rel ( pwd , v . ( string ) )
if err != nil {
return v . ( string )
}
return rel
} ,
2015-11-13 18:07:02 +01:00
Deprecated : "Use the 'template' attribute instead." ,
ConflictsWith : [ ] string { "template" } ,
2015-05-02 01:59:16 +02:00
} ,
"vars" : & schema . Schema {
Type : schema . TypeMap ,
Optional : true ,
Default : make ( map [ string ] interface { } ) ,
Description : "variables to substitute" ,
2015-05-09 00:46:37 +02:00
ForceNew : true ,
2015-05-02 01:59:16 +02:00
} ,
"rendered" : & schema . Schema {
Type : schema . TypeString ,
Computed : true ,
Description : "rendered template" ,
} ,
} ,
}
}
2015-10-06 01:08:33 +02:00
func resourceFileCreate ( d * schema . ResourceData , meta interface { } ) error {
rendered , err := renderFile ( d )
2015-05-09 00:46:37 +02:00
if err != nil {
return err
}
d . Set ( "rendered" , rendered )
d . SetId ( hash ( rendered ) )
return nil
}
2015-10-06 01:08:33 +02:00
func resourceFileDelete ( d * schema . ResourceData , meta interface { } ) error {
2015-05-02 01:59:16 +02:00
d . SetId ( "" )
return nil
}
2015-05-09 00:46:37 +02:00
2015-10-06 01:08:33 +02:00
func resourceFileExists ( d * schema . ResourceData , meta interface { } ) ( bool , error ) {
rendered , err := renderFile ( d )
2015-05-09 00:46:37 +02:00
if err != nil {
2015-06-17 20:58:01 +02:00
if _ , ok := err . ( templateRenderError ) ; ok {
log . Printf ( "[DEBUG] Got error while rendering in Exists: %s" , err )
log . Printf ( "[DEBUG] Returning false so the template re-renders using latest variables from config." )
return false , nil
} else {
return false , err
}
2015-05-09 00:46:37 +02:00
}
return hash ( rendered ) == d . Id ( ) , nil
}
2015-10-06 01:08:33 +02:00
func resourceFileRead ( d * schema . ResourceData , meta interface { } ) error {
2015-05-09 00:46:37 +02:00
// Logic is handled in Exists, which only returns true if the rendered
// contents haven't changed. That means if we get here there's nothing to
// do.
return nil
2015-05-02 01:59:16 +02:00
}
2015-06-17 20:58:01 +02:00
type templateRenderError error
2015-10-06 01:08:33 +02:00
func renderFile ( d * schema . ResourceData ) ( string , error ) {
2015-11-13 18:07:02 +01:00
template := d . Get ( "template" ) . ( string )
2015-05-02 01:59:16 +02:00
filename := d . Get ( "filename" ) . ( string )
vars := d . Get ( "vars" ) . ( map [ string ] interface { } )
2015-11-13 18:07:02 +01:00
if template == "" && filename != "" {
template = filename
2015-05-04 19:26:17 +02:00
}
2015-11-13 18:07:02 +01:00
contents , _ , err := pathorcontents . Read ( template )
2015-05-02 01:59:16 +02:00
if err != nil {
2015-05-09 00:46:37 +02:00
return "" , err
2015-05-02 01:59:16 +02:00
}
2015-11-13 18:07:02 +01:00
rendered , err := execute ( contents , vars )
2015-05-02 01:59:16 +02:00
if err != nil {
2015-06-17 20:58:01 +02:00
return "" , templateRenderError (
fmt . Errorf ( "failed to render %v: %v" , filename , err ) ,
)
2015-05-02 01:59:16 +02:00
}
2015-05-09 00:46:37 +02:00
return rendered , nil
2015-05-02 01:59:16 +02:00
}
// execute parses and executes a template using vars.
func execute ( s string , vars map [ string ] interface { } ) ( string , error ) {
2016-01-31 08:45:15 +01:00
root , err := hil . Parse ( s )
2015-05-02 01:59:16 +02:00
if err != nil {
return "" , err
}
varmap := make ( map [ string ] ast . Variable )
for k , v := range vars {
// As far as I can tell, v is always a string.
// If it's not, tell the user gracefully.
s , ok := v . ( string )
if ! ok {
return "" , fmt . Errorf ( "unexpected type for variable %q: %T" , k , v )
}
varmap [ k ] = ast . Variable {
Value : s ,
Type : ast . TypeString ,
}
}
2016-01-31 08:45:15 +01:00
cfg := hil . EvalConfig {
2015-05-02 01:59:16 +02:00
GlobalScope : & ast . BasicScope {
VarMap : varmap ,
2016-01-15 22:28:47 +01:00
FuncMap : config . Funcs ( ) ,
2015-05-02 01:59:16 +02:00
} ,
}
2016-01-31 08:45:15 +01:00
out , typ , err := hil . Eval ( root , & cfg )
2015-05-02 01:59:16 +02:00
if err != nil {
return "" , err
}
if typ != ast . TypeString {
return "" , fmt . Errorf ( "unexpected output ast.Type: %v" , typ )
}
return out . ( string ) , nil
}
func hash ( s string ) string {
sha := sha256 . Sum256 ( [ ] byte ( s ) )
2015-05-09 00:46:37 +02:00
return hex . EncodeToString ( sha [ : ] )
2015-05-02 01:59:16 +02:00
}
2016-03-10 19:34:56 +01:00
func validateTemplateAttribute ( v interface { } , key string ) ( ws [ ] string , es [ ] error ) {
_ , wasPath , err := pathorcontents . Read ( v . ( string ) )
if err != nil {
es = append ( es , err )
return
}
if wasPath {
ws = append ( ws , fmt . Sprintf ( "%s: looks like you specified a path instead of file contents. Use `file()` to load this path. Specifying a path directly is deprecated and will be removed in a future version." , key ) )
}
return
}