github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/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 out, typ, err := hil.Eval(root, &cfg) 164 if err != nil { 165 return "", err 166 } 167 if typ != ast.TypeString { 168 return "", fmt.Errorf("unexpected output ast.Type: %v", typ) 169 } 170 171 return out.(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 }