mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-10 08:03:08 -06:00
385b17d679
The Exists function can run in a context where the contents of the template have changed, but it uses the old set of variables from the state. This means that when the set of variables changes, rendering will fail in Exists. This was returning an error, but really it just needs to be treated as a scenario where the template needs re-rendering. fixes #2344 and possibly a few other template issues floating around
168 lines
3.7 KiB
Go
168 lines
3.7 KiB
Go
package template
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/hashicorp/terraform/config"
|
|
"github.com/hashicorp/terraform/config/lang"
|
|
"github.com/hashicorp/terraform/config/lang/ast"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
"github.com/mitchellh/go-homedir"
|
|
)
|
|
|
|
func resource() *schema.Resource {
|
|
return &schema.Resource{
|
|
Create: Create,
|
|
Delete: Delete,
|
|
Exists: Exists,
|
|
Read: Read,
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"filename": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
Description: "file to read template from",
|
|
ForceNew: true,
|
|
// Make a "best effort" attempt to relativize the file path.
|
|
StateFunc: func(v interface{}) string {
|
|
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
|
|
},
|
|
},
|
|
"vars": &schema.Schema{
|
|
Type: schema.TypeMap,
|
|
Optional: true,
|
|
Default: make(map[string]interface{}),
|
|
Description: "variables to substitute",
|
|
ForceNew: true,
|
|
},
|
|
"rendered": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Description: "rendered template",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func Create(d *schema.ResourceData, meta interface{}) error {
|
|
rendered, err := render(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.Set("rendered", rendered)
|
|
d.SetId(hash(rendered))
|
|
return nil
|
|
}
|
|
|
|
func Delete(d *schema.ResourceData, meta interface{}) error {
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
func Exists(d *schema.ResourceData, meta interface{}) (bool, error) {
|
|
rendered, err := render(d)
|
|
if err != nil {
|
|
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
|
|
}
|
|
}
|
|
return hash(rendered) == d.Id(), nil
|
|
}
|
|
|
|
func Read(d *schema.ResourceData, meta interface{}) error {
|
|
// 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
|
|
}
|
|
|
|
type templateRenderError error
|
|
|
|
var readfile func(string) ([]byte, error) = ioutil.ReadFile // testing hook
|
|
|
|
func render(d *schema.ResourceData) (string, error) {
|
|
filename := d.Get("filename").(string)
|
|
vars := d.Get("vars").(map[string]interface{})
|
|
|
|
path, err := homedir.Expand(filename)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
buf, err := readfile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
rendered, err := execute(string(buf), vars)
|
|
if err != nil {
|
|
return "", templateRenderError(
|
|
fmt.Errorf("failed to render %v: %v", filename, err),
|
|
)
|
|
}
|
|
|
|
return rendered, nil
|
|
}
|
|
|
|
// execute parses and executes a template using vars.
|
|
func execute(s string, vars map[string]interface{}) (string, error) {
|
|
root, err := lang.Parse(s)
|
|
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,
|
|
}
|
|
}
|
|
|
|
cfg := lang.EvalConfig{
|
|
GlobalScope: &ast.BasicScope{
|
|
VarMap: varmap,
|
|
FuncMap: config.Funcs,
|
|
},
|
|
}
|
|
|
|
out, typ, err := lang.Eval(root, &cfg)
|
|
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))
|
|
return hex.EncodeToString(sha[:])
|
|
}
|