github.com/jrasell/terraform@v0.6.17-0.20160523115548-2652f5232949/builtin/providers/template/resource_template_file.go (about)

     1  package template
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/hashicorp/hil"
    12  	"github.com/hashicorp/hil/ast"
    13  	"github.com/hashicorp/terraform/config"
    14  	"github.com/hashicorp/terraform/helper/pathorcontents"
    15  	"github.com/hashicorp/terraform/helper/schema"
    16  )
    17  
    18  func resourceFile() *schema.Resource {
    19  	return &schema.Resource{
    20  		Create: resourceFileCreate,
    21  		Delete: resourceFileDelete,
    22  		Exists: resourceFileExists,
    23  		Read:   resourceFileRead,
    24  
    25  		Schema: map[string]*schema.Schema{
    26  			"template": &schema.Schema{
    27  				Type:          schema.TypeString,
    28  				Optional:      true,
    29  				Description:   "Contents of the template",
    30  				ForceNew:      true,
    31  				ConflictsWith: []string{"filename"},
    32  				ValidateFunc:  validateTemplateAttribute,
    33  			},
    34  			"filename": &schema.Schema{
    35  				Type:        schema.TypeString,
    36  				Optional:    true,
    37  				Description: "file to read template from",
    38  				ForceNew:    true,
    39  				// Make a "best effort" attempt to relativize the file path.
    40  				StateFunc: func(v interface{}) string {
    41  					if v == nil || v.(string) == "" {
    42  						return ""
    43  					}
    44  					pwd, err := os.Getwd()
    45  					if err != nil {
    46  						return v.(string)
    47  					}
    48  					rel, err := filepath.Rel(pwd, v.(string))
    49  					if err != nil {
    50  						return v.(string)
    51  					}
    52  					return rel
    53  				},
    54  				Deprecated:    "Use the 'template' attribute instead.",
    55  				ConflictsWith: []string{"template"},
    56  			},
    57  			"vars": &schema.Schema{
    58  				Type:        schema.TypeMap,
    59  				Optional:    true,
    60  				Default:     make(map[string]interface{}),
    61  				Description: "variables to substitute",
    62  				ForceNew:    true,
    63  			},
    64  			"rendered": &schema.Schema{
    65  				Type:        schema.TypeString,
    66  				Computed:    true,
    67  				Description: "rendered template",
    68  			},
    69  		},
    70  	}
    71  }
    72  
    73  func resourceFileCreate(d *schema.ResourceData, meta interface{}) error {
    74  	rendered, err := renderFile(d)
    75  	if err != nil {
    76  		return err
    77  	}
    78  	d.Set("rendered", rendered)
    79  	d.SetId(hash(rendered))
    80  	return nil
    81  }
    82  
    83  func resourceFileDelete(d *schema.ResourceData, meta interface{}) error {
    84  	d.SetId("")
    85  	return nil
    86  }
    87  
    88  func resourceFileExists(d *schema.ResourceData, meta interface{}) (bool, error) {
    89  	rendered, err := renderFile(d)
    90  	if err != nil {
    91  		if _, ok := err.(templateRenderError); ok {
    92  			log.Printf("[DEBUG] Got error while rendering in Exists: %s", err)
    93  			log.Printf("[DEBUG] Returning false so the template re-renders using latest variables from config.")
    94  			return false, nil
    95  		} else {
    96  			return false, err
    97  		}
    98  	}
    99  	return hash(rendered) == d.Id(), nil
   100  }
   101  
   102  func resourceFileRead(d *schema.ResourceData, meta interface{}) error {
   103  	// Logic is handled in Exists, which only returns true if the rendered
   104  	// contents haven't changed. That means if we get here there's nothing to
   105  	// do.
   106  	return nil
   107  }
   108  
   109  type templateRenderError error
   110  
   111  func renderFile(d *schema.ResourceData) (string, error) {
   112  	template := d.Get("template").(string)
   113  	filename := d.Get("filename").(string)
   114  	vars := d.Get("vars").(map[string]interface{})
   115  
   116  	if template == "" && filename != "" {
   117  		template = filename
   118  	}
   119  
   120  	contents, _, err := pathorcontents.Read(template)
   121  	if err != nil {
   122  		return "", err
   123  	}
   124  
   125  	rendered, err := execute(contents, vars)
   126  	if err != nil {
   127  		return "", templateRenderError(
   128  			fmt.Errorf("failed to render %v: %v", filename, err),
   129  		)
   130  	}
   131  
   132  	return rendered, nil
   133  }
   134  
   135  // execute parses and executes a template using vars.
   136  func execute(s string, vars map[string]interface{}) (string, error) {
   137  	root, err := hil.Parse(s)
   138  	if err != nil {
   139  		return "", err
   140  	}
   141  
   142  	varmap := make(map[string]ast.Variable)
   143  	for k, v := range vars {
   144  		// As far as I can tell, v is always a string.
   145  		// If it's not, tell the user gracefully.
   146  		s, ok := v.(string)
   147  		if !ok {
   148  			return "", fmt.Errorf("unexpected type for variable %q: %T", k, v)
   149  		}
   150  		varmap[k] = ast.Variable{
   151  			Value: s,
   152  			Type:  ast.TypeString,
   153  		}
   154  	}
   155  
   156  	cfg := hil.EvalConfig{
   157  		GlobalScope: &ast.BasicScope{
   158  			VarMap:  varmap,
   159  			FuncMap: config.Funcs(),
   160  		},
   161  	}
   162  
   163  	result, err := hil.Eval(root, &cfg)
   164  	if err != nil {
   165  		return "", err
   166  	}
   167  	if result.Type != hil.TypeString {
   168  		return "", fmt.Errorf("unexpected output hil.Type: %v", result.Type)
   169  	}
   170  
   171  	return result.Value.(string), nil
   172  }
   173  
   174  func hash(s string) string {
   175  	sha := sha256.Sum256([]byte(s))
   176  	return hex.EncodeToString(sha[:])
   177  }
   178  
   179  func validateTemplateAttribute(v interface{}, key string) (ws []string, es []error) {
   180  	_, wasPath, err := pathorcontents.Read(v.(string))
   181  	if err != nil {
   182  		es = append(es, err)
   183  		return
   184  	}
   185  
   186  	if wasPath {
   187  		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))
   188  	}
   189  
   190  	return
   191  }