Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

providers: add template provider #1778

Merged
merged 3 commits into from
May 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions builtin/bins/provider-template/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

import (
"github.com/hashicorp/terraform/builtin/providers/template"
"github.com/hashicorp/terraform/plugin"
)

func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: template.Provider,
})
}
14 changes: 14 additions & 0 deletions builtin/providers/template/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package template

import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)

func Provider() terraform.ResourceProvider {
return &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"template_file": resource(),
},
}
}
13 changes: 13 additions & 0 deletions builtin/providers/template/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package template

import (
"testing"

"github.com/hashicorp/terraform/helper/schema"
)

func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
126 changes: 126 additions & 0 deletions builtin/providers/template/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package template

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"

"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,
Read: Read,
Update: Update,
Delete: Delete,
Exists: Exists,

Schema: map[string]*schema.Schema{
"filename": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "file to read template from",
},
"vars": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Default: make(map[string]interface{}),
Description: "variables to substitute",
},
"rendered": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Description: "rendered template",
},
},
}
}

func Create(d *schema.ResourceData, meta interface{}) error { return eval(d) }
func Update(d *schema.ResourceData, meta interface{}) error { return eval(d) }
func Read(d *schema.ResourceData, meta interface{}) error { return nil }
func Delete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
func Exists(d *schema.ResourceData, meta interface{}) (bool, error) {
// Reload every time in case something has changed.
// This should be cheap, and cache invalidation is hard.
return false, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just omit Exists, which has the same effect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nevermind - didn't see that Read was a noop. This makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @josharian - working on #1866 and coming back around to this. I think we need to rework the lifecycle a bit here. Will follow up with a PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Sorry about that. Let me know if you'd like me to dive in. (Sounds from your comment like you have it under control.)


var readfile func(string) ([]byte, error) = ioutil.ReadFile // testing hook

func eval(d *schema.ResourceData) 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 fmt.Errorf("failed to render %v: %v", filename, err)
}

d.Set("rendered", rendered)
d.SetId(hash(rendered))
return 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[:])[:20]
}
58 changes: 58 additions & 0 deletions builtin/providers/template/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package template

import (
"fmt"
"testing"

r "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

var testProviders = map[string]terraform.ResourceProvider{
"template": Provider(),
}

func TestTemplateRendering(t *testing.T) {
var cases = []struct {
vars string
template string
want string
}{
{`{}`, `ABC`, `ABC`},
{`{a="foo"}`, `${a}`, `foo`},
{`{a="hello"}`, `${replace(a, "ello", "i")}`, `hi`},
{`{}`, `${1+2+3}`, `6`},
}

for _, tt := range cases {
r.Test(t, r.TestCase{
PreCheck: func() {
readfile = func(string) ([]byte, error) {
return []byte(tt.template), nil
}
},
Providers: testProviders,
Steps: []r.TestStep{
r.TestStep{
Config: `
resource "template_file" "t0" {
filename = "mock"
vars = ` + tt.vars + `
}
output "rendered" {
value = "${template_file.t0.rendered}"
}
`,
Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["rendered"]
if tt.want != got {
return fmt.Errorf("template:\n%s\nvars:\n%s\ngot:\n%s\nwant:\n%s\n", tt.template, tt.vars, got, tt.want)
}
return nil
},
TransientResource: true,
},
},
})
}
}
8 changes: 7 additions & 1 deletion helper/resource/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ type TestStep struct {

// Destroy will create a destroy plan if set to true.
Destroy bool

// TransientResource indicates that resources created as part
// of this test step are temporary and might be recreated anew
// with every planning step. This should only be set for
// pseudo-resources, like the null resource or templates.
TransientResource bool
}

// Test performs an acceptance test on a resource.
Expand Down Expand Up @@ -260,7 +266,7 @@ func testStep(
if p, err := ctx.Plan(); err != nil {
return state, fmt.Errorf("Error on second follow-up plan: %s", err)
} else {
if p.Diff != nil && !p.Diff.Empty() {
if p.Diff != nil && !p.Diff.Empty() && !step.TransientResource {
return state, fmt.Errorf(
"After applying this step and refreshing, the plan was not empty:\n\n%s", p)
}
Expand Down
30 changes: 30 additions & 0 deletions website/source/docs/configuration/interpolation.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,33 @@ The supported built-in functions are:
back into a list. This is useful for pushing lists through module
outputs since they currently only support string values.
Example: `split(",", module.amod.server_ids)`

## Templates

Long strings can be managed using templates. Templates are [resources](/docs/configuration/resources.html) defined by a filename and some variables to use during interpolation. They have a computed `rendered` attribute containing the result.

A template resource looks like:

```
resource "template_file" "example" {
filename = "template.txt"
vars {
hello = "goodnight"
world = "moon"
}
}

output "rendered" {
value = "${template_file.example.rendered}"
}
```

Assuming `template.txt` looks like this:

```
${hello} ${world}!
```

Then the rendered value would be `goodnight moon!`.

You may use any of the built-in functions in your template.
2 changes: 1 addition & 1 deletion website/source/docs/configuration/providers.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: |-
# Provider Configuration

Providers are responsible in Terraform for managing the lifecycle
of a [resource](/docs/configuration/resource.html): create,
of a [resource](/docs/configuration/resources.html): create,
read, update, delete.

Every resource in Terraform is mapped to a provider based
Expand Down