Merge pull request #1778 from josharian/template-provider
providers: add template provider
This commit is contained in:
commit
cebcee5c63
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,12 @@ type TestStep struct {
|
||||||
|
|
||||||
// Destroy will create a destroy plan if set to true.
|
// Destroy will create a destroy plan if set to true.
|
||||||
Destroy bool
|
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.
|
// Test performs an acceptance test on a resource.
|
||||||
|
@ -260,7 +266,7 @@ func testStep(
|
||||||
if p, err := ctx.Plan(); err != nil {
|
if p, err := ctx.Plan(); err != nil {
|
||||||
return state, fmt.Errorf("Error on second follow-up plan: %s", err)
|
return state, fmt.Errorf("Error on second follow-up plan: %s", err)
|
||||||
} else {
|
} else {
|
||||||
if p.Diff != nil && !p.Diff.Empty() {
|
if p.Diff != nil && !p.Diff.Empty() && !step.TransientResource {
|
||||||
return state, fmt.Errorf(
|
return state, fmt.Errorf(
|
||||||
"After applying this step and refreshing, the plan was not empty:\n\n%s", p)
|
"After applying this step and refreshing, the plan was not empty:\n\n%s", p)
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,3 +114,33 @@ The supported built-in functions are:
|
||||||
back into a list. This is useful for pushing lists through module
|
back into a list. This is useful for pushing lists through module
|
||||||
outputs since they currently only support string values.
|
outputs since they currently only support string values.
|
||||||
Example: `split(",", module.amod.server_ids)`
|
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.
|
||||||
|
|
|
@ -9,7 +9,7 @@ description: |-
|
||||||
# Provider Configuration
|
# Provider Configuration
|
||||||
|
|
||||||
Providers are responsible in Terraform for managing the lifecycle
|
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.
|
read, update, delete.
|
||||||
|
|
||||||
Every resource in Terraform is mapped to a provider based
|
Every resource in Terraform is mapped to a provider based
|
||||||
|
|
Loading…
Reference in New Issue