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