github.com/armen/terraform@v0.5.2-0.20150529052519-caa8117a08f1/builtin/providers/template/resource.go (about) 1 package template 2 3 import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "fmt" 7 "io/ioutil" 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/schema" 15 "github.com/mitchellh/go-homedir" 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 "filename": &schema.Schema{ 27 Type: schema.TypeString, 28 Required: true, 29 Description: "file to read template from", 30 ForceNew: true, 31 // Make a "best effort" attempt to relativize the file path. 32 StateFunc: func(v interface{}) string { 33 pwd, err := os.Getwd() 34 if err != nil { 35 return v.(string) 36 } 37 rel, err := filepath.Rel(pwd, v.(string)) 38 if err != nil { 39 return v.(string) 40 } 41 return rel 42 }, 43 }, 44 "vars": &schema.Schema{ 45 Type: schema.TypeMap, 46 Optional: true, 47 Default: make(map[string]interface{}), 48 Description: "variables to substitute", 49 ForceNew: true, 50 }, 51 "rendered": &schema.Schema{ 52 Type: schema.TypeString, 53 Computed: true, 54 Description: "rendered template", 55 }, 56 }, 57 } 58 } 59 60 func Create(d *schema.ResourceData, meta interface{}) error { 61 rendered, err := render(d) 62 if err != nil { 63 return err 64 } 65 d.Set("rendered", rendered) 66 d.SetId(hash(rendered)) 67 return nil 68 } 69 70 func Delete(d *schema.ResourceData, meta interface{}) error { 71 d.SetId("") 72 return nil 73 } 74 75 func Exists(d *schema.ResourceData, meta interface{}) (bool, error) { 76 rendered, err := render(d) 77 if err != nil { 78 return false, err 79 } 80 return hash(rendered) == d.Id(), nil 81 } 82 83 func Read(d *schema.ResourceData, meta interface{}) error { 84 // Logic is handled in Exists, which only returns true if the rendered 85 // contents haven't changed. That means if we get here there's nothing to 86 // do. 87 return nil 88 } 89 90 var readfile func(string) ([]byte, error) = ioutil.ReadFile // testing hook 91 92 func render(d *schema.ResourceData) (string, error) { 93 filename := d.Get("filename").(string) 94 vars := d.Get("vars").(map[string]interface{}) 95 96 path, err := homedir.Expand(filename) 97 if err != nil { 98 return "", err 99 } 100 101 buf, err := readfile(path) 102 if err != nil { 103 return "", err 104 } 105 106 rendered, err := execute(string(buf), vars) 107 if err != nil { 108 return "", fmt.Errorf("failed to render %v: %v", filename, err) 109 } 110 111 return rendered, nil 112 } 113 114 // execute parses and executes a template using vars. 115 func execute(s string, vars map[string]interface{}) (string, error) { 116 root, err := lang.Parse(s) 117 if err != nil { 118 return "", err 119 } 120 121 varmap := make(map[string]ast.Variable) 122 for k, v := range vars { 123 // As far as I can tell, v is always a string. 124 // If it's not, tell the user gracefully. 125 s, ok := v.(string) 126 if !ok { 127 return "", fmt.Errorf("unexpected type for variable %q: %T", k, v) 128 } 129 varmap[k] = ast.Variable{ 130 Value: s, 131 Type: ast.TypeString, 132 } 133 } 134 135 cfg := lang.EvalConfig{ 136 GlobalScope: &ast.BasicScope{ 137 VarMap: varmap, 138 FuncMap: config.Funcs, 139 }, 140 } 141 142 out, typ, err := lang.Eval(root, &cfg) 143 if err != nil { 144 return "", err 145 } 146 if typ != ast.TypeString { 147 return "", fmt.Errorf("unexpected output ast.Type: %v", typ) 148 } 149 150 return out.(string), nil 151 } 152 153 func hash(s string) string { 154 sha := sha256.Sum256([]byte(s)) 155 return hex.EncodeToString(sha[:]) 156 }