provider/template: don't error when rendering fails in Exists

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
This commit is contained in:
Paul Hinze 2015-06-17 13:58:01 -05:00
parent f0d8682df6
commit 385b17d679
3 changed files with 74 additions and 11 deletions

View File

@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
@ -75,8 +76,14 @@ func Delete(d *schema.ResourceData, meta interface{}) error {
func Exists(d *schema.ResourceData, meta interface{}) (bool, error) { func Exists(d *schema.ResourceData, meta interface{}) (bool, error) {
rendered, err := render(d) rendered, err := render(d)
if err != nil { 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 false, err
} }
}
return hash(rendered) == d.Id(), nil return hash(rendered) == d.Id(), nil
} }
@ -87,6 +94,8 @@ func Read(d *schema.ResourceData, meta interface{}) error {
return nil return nil
} }
type templateRenderError error
var readfile func(string) ([]byte, error) = ioutil.ReadFile // testing hook var readfile func(string) ([]byte, error) = ioutil.ReadFile // testing hook
func render(d *schema.ResourceData) (string, error) { func render(d *schema.ResourceData) (string, error) {
@ -105,7 +114,9 @@ func render(d *schema.ResourceData) (string, error) {
rendered, err := execute(string(buf), vars) rendered, err := execute(string(buf), vars)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to render %v: %v", filename, err) return "", templateRenderError(
fmt.Errorf("failed to render %v: %v", filename, err),
)
} }
return rendered, nil return rendered, nil

View File

@ -34,15 +34,7 @@ func TestTemplateRendering(t *testing.T) {
Providers: testProviders, Providers: testProviders,
Steps: []r.TestStep{ Steps: []r.TestStep{
r.TestStep{ r.TestStep{
Config: ` Config: testTemplateConfig(tt.vars),
resource "template_file" "t0" {
filename = "mock"
vars = ` + tt.vars + `
}
output "rendered" {
value = "${template_file.t0.rendered}"
}
`,
Check: func(s *terraform.State) error { Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["rendered"] got := s.RootModule().Outputs["rendered"]
if tt.want != got { if tt.want != got {
@ -55,3 +47,55 @@ output "rendered" {
}) })
} }
} }
// https://github.com/hashicorp/terraform/issues/2344
func TestTemplateVariableChange(t *testing.T) {
steps := []struct {
vars string
template string
want string
}{
{`{a="foo"}`, `${a}`, `foo`},
{`{b="bar"}`, `${b}`, `bar`},
}
var testSteps []r.TestStep
for i, step := range steps {
testSteps = append(testSteps, r.TestStep{
PreConfig: func(template string) func() {
return func() {
readfile = func(string) ([]byte, error) {
return []byte(template), nil
}
}
}(step.template),
Config: testTemplateConfig(step.vars),
Check: func(i int, want string) r.TestCheckFunc {
return func(s *terraform.State) error {
got := s.RootModule().Outputs["rendered"]
if want != got {
return fmt.Errorf("[%d] got:\n%q\nwant:\n%q\n", i, got, want)
}
return nil
}
}(i, step.want),
})
}
r.Test(t, r.TestCase{
Providers: testProviders,
Steps: testSteps,
})
}
func testTemplateConfig(vars string) string {
return `
resource "template_file" "t0" {
filename = "mock"
vars = ` + vars + `
}
output "rendered" {
value = "${template_file.t0.rendered}"
}
`
}

View File

@ -60,6 +60,10 @@ type TestCase struct {
// potentially complex update logic. In general, simply create/destroy // potentially complex update logic. In general, simply create/destroy
// tests will only need one step. // tests will only need one step.
type TestStep struct { type TestStep struct {
// PreConfig is called before the Config is applied to perform any per-step
// setup that needs to happen
PreConfig func()
// Config a string of the configuration to give to Terraform. // Config a string of the configuration to give to Terraform.
Config string Config string
@ -160,6 +164,10 @@ func testStep(
opts terraform.ContextOpts, opts terraform.ContextOpts,
state *terraform.State, state *terraform.State,
step TestStep) (*terraform.State, error) { step TestStep) (*terraform.State, error) {
if step.PreConfig != nil {
step.PreConfig()
}
cfgPath, err := ioutil.TempDir("", "tf-test") cfgPath, err := ioutil.TempDir("", "tf-test")
if err != nil { if err != nil {
return state, fmt.Errorf( return state, fmt.Errorf(