provider/template: convert resources to data sources

The template resources don't actually need to retain any state, so they
are good candidates to be data sources.

This includes a few tweaks to the acceptance tests -- now configured to
run as unit tests -- since it seems that they have been slightly broken
for a while now. In particular, the "update" cases are no longer tested
because updating is not a meaningful operation for a data source.
This commit is contained in:
Martin Atkins 2016-05-21 15:09:55 -07:00 committed by James Nugent
parent 46e3cacee3
commit 861ac536dd
11 changed files with 122 additions and 262 deletions

View File

@ -15,13 +15,9 @@ import (
"github.com/sthulb/mime/multipart"
)
func resourceCloudinitConfig() *schema.Resource {
func dataSourceCloudinitConfig() *schema.Resource {
return &schema.Resource{
Create: resourceCloudinitConfigCreate,
Delete: resourceCloudinitConfigDelete,
Update: resourceCloudinitConfigCreate,
Exists: resourceCloudinitConfigExists,
Read: resourceCloudinitConfigRead,
Read: dataSourceCloudinitConfigRead,
Schema: map[string]*schema.Schema{
"part": &schema.Schema{
@ -52,13 +48,11 @@ func resourceCloudinitConfig() *schema.Resource {
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"base64_encode": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"rendered": &schema.Schema{
Type: schema.TypeString,
@ -69,7 +63,7 @@ func resourceCloudinitConfig() *schema.Resource {
}
}
func resourceCloudinitConfigCreate(d *schema.ResourceData, meta interface{}) error {
func dataSourceCloudinitConfigRead(d *schema.ResourceData, meta interface{}) error {
rendered, err := renderCloudinitConfig(d)
if err != nil {
return err
@ -80,24 +74,6 @@ func resourceCloudinitConfigCreate(d *schema.ResourceData, meta interface{}) err
return nil
}
func resourceCloudinitConfigDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
func resourceCloudinitConfigExists(d *schema.ResourceData, meta interface{}) (bool, error) {
rendered, err := renderCloudinitConfig(d)
if err != nil {
return false, err
}
return strconv.Itoa(hashcode.String(rendered)) == d.Id(), nil
}
func resourceCloudinitConfigRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func renderCloudinitConfig(d *schema.ResourceData) (string, error) {
gzipOutput := d.Get("gzip").(bool)
base64Output := d.Get("base64_encode").(bool)

View File

@ -0,0 +1,80 @@
package template
import (
"testing"
r "github.com/hashicorp/terraform/helper/resource"
)
func TestRender(t *testing.T) {
testCases := []struct {
ResourceBlock string
Expected string
}{
{
`data "template_cloudinit_config" "foo" {
gzip = false
base64_encode = false
part {
content_type = "text/x-shellscript"
content = "baz"
}
}`,
"Content-Type: multipart/mixed; boundary=\"MIMEBOUNDARY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDARY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDARY--\r\n",
},
{
`data "template_cloudinit_config" "foo" {
gzip = false
base64_encode = false
part {
content_type = "text/x-shellscript"
content = "baz"
filename = "foobar.sh"
}
}`,
"Content-Type: multipart/mixed; boundary=\"MIMEBOUNDARY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDARY\r\nContent-Disposition: attachment; filename=\"foobar.sh\"\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDARY--\r\n",
},
{
`data "template_cloudinit_config" "foo" {
gzip = false
base64_encode = false
part {
content_type = "text/x-shellscript"
content = "baz"
}
part {
content_type = "text/x-shellscript"
content = "ffbaz"
}
}`,
"Content-Type: multipart/mixed; boundary=\"MIMEBOUNDARY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDARY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDARY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nffbaz\r\n--MIMEBOUNDARY--\r\n",
},
}
for _, tt := range testCases {
r.UnitTest(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
r.TestStep{
Config: tt.ResourceBlock,
Check: r.ComposeTestCheckFunc(
r.TestCheckResourceAttr("data.template_cloudinit_config.foo", "rendered", tt.Expected),
),
},
},
})
}
}
var testCloudInitConfig_basic = `
data "template_cloudinit_config" "config" {
part {
content_type = "text/x-shellscript"
content = "baz"
}
}`
var testCloudInitConfig_basic_expected = `Content-Type: multipart/mixed; boundary=\"MIMEBOUNDARY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDARY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDARY--\r\n`

View File

@ -4,7 +4,6 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
@ -15,19 +14,15 @@ import (
"github.com/hashicorp/terraform/helper/schema"
)
func resourceFile() *schema.Resource {
func dataSourceFile() *schema.Resource {
return &schema.Resource{
Create: resourceFileCreate,
Delete: resourceFileDelete,
Exists: resourceFileExists,
Read: resourceFileRead,
Read: dataSourceFileRead,
Schema: map[string]*schema.Schema{
"template": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Contents of the template",
ForceNew: true,
ConflictsWith: []string{"filename"},
ValidateFunc: validateTemplateAttribute,
},
@ -35,7 +30,6 @@ func resourceFile() *schema.Resource {
Type: schema.TypeString,
Optional: true,
Description: "file to read template from",
ForceNew: true,
// Make a "best effort" attempt to relativize the file path.
StateFunc: func(v interface{}) string {
if v == nil || v.(string) == "" {
@ -59,7 +53,6 @@ func resourceFile() *schema.Resource {
Optional: true,
Default: make(map[string]interface{}),
Description: "variables to substitute",
ForceNew: true,
},
"rendered": &schema.Schema{
Type: schema.TypeString,
@ -70,7 +63,7 @@ func resourceFile() *schema.Resource {
}
}
func resourceFileCreate(d *schema.ResourceData, meta interface{}) error {
func dataSourceFileRead(d *schema.ResourceData, meta interface{}) error {
rendered, err := renderFile(d)
if err != nil {
return err
@ -80,32 +73,6 @@ func resourceFileCreate(d *schema.ResourceData, meta interface{}) error {
return nil
}
func resourceFileDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
func resourceFileExists(d *schema.ResourceData, meta interface{}) (bool, error) {
rendered, err := renderFile(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 resourceFileRead(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
func renderFile(d *schema.ResourceData) (string, error) {

View File

@ -23,13 +23,13 @@ func TestTemplateRendering(t *testing.T) {
want string
}{
{`{}`, `ABC`, `ABC`},
{`{a="foo"}`, `${a}`, `foo`},
{`{a="hello"}`, `${replace(a, "ello", "i")}`, `hi`},
{`{a="foo"}`, `$${a}`, `foo`},
{`{a="hello"}`, `$${replace(a, "ello", "i")}`, `hi`},
{`{}`, `${1+2+3}`, `6`},
}
for _, tt := range cases {
r.Test(t, r.TestCase{
r.UnitTest(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
r.TestStep{
@ -47,39 +47,6 @@ func TestTemplateRendering(t *testing.T) {
}
}
// 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{
Config: testTemplateConfig(step.template, step.vars),
Check: func(i int, want string) r.TestCheckFunc {
return func(s *terraform.State) error {
got := s.RootModule().Outputs["rendered"]
if want != got.Value {
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 TestValidateTemplateAttribute(t *testing.T) {
file, err := ioutil.TempFile("", "testtemplate")
if err != nil {
@ -129,11 +96,11 @@ func TestTemplateSharedMemoryRace(t *testing.T) {
func testTemplateConfig(template, vars string) string {
return fmt.Sprintf(`
resource "template_file" "t0" {
data "template_file" "t0" {
template = "%s"
vars = %s
}
output "rendered" {
value = "${template_file.t0.rendered}"
value = "${data.template_file.t0.rendered}"
}`, template, vars)
}

View File

@ -7,9 +7,19 @@ import (
func Provider() terraform.ResourceProvider {
return &schema.Provider{
DataSourcesMap: map[string]*schema.Resource{
"template_file": dataSourceFile(),
"template_cloudinit_config": dataSourceCloudinitConfig(),
},
ResourcesMap: map[string]*schema.Resource{
"template_file": resourceFile(),
"template_cloudinit_config": resourceCloudinitConfig(),
"template_file": schema.DataSourceResourceShim(
"template_file",
dataSourceFile(),
),
"template_cloudinit_config": schema.DataSourceResourceShim(
"template_cloudinit_config",
dataSourceCloudinitConfig(),
),
},
}
}

View File

@ -1,133 +0,0 @@
package template
import (
"testing"
r "github.com/hashicorp/terraform/helper/resource"
)
func TestRender(t *testing.T) {
testCases := []struct {
ResourceBlock string
Expected string
}{
{
`resource "template_cloudinit_config" "foo" {
gzip = false
base64_encode = false
part {
content_type = "text/x-shellscript"
content = "baz"
}
}`,
"Content-Type: multipart/mixed; boundary=\"MIMEBOUNDRY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDRY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDRY--\r\n",
},
{
`resource "template_cloudinit_config" "foo" {
gzip = false
base64_encode = false
part {
content_type = "text/x-shellscript"
content = "baz"
filename = "foobar.sh"
}
}`,
"Content-Type: multipart/mixed; boundary=\"MIMEBOUNDRY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDRY\r\nContent-Disposition: attachment; filename=\"foobar.sh\"\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDRY--\r\n",
},
{
`resource "template_cloudinit_config" "foo" {
gzip = false
base64_encode = false
part {
content_type = "text/x-shellscript"
content = "baz"
}
part {
content_type = "text/x-shellscript"
content = "ffbaz"
}
}`,
"Content-Type: multipart/mixed; boundary=\"MIMEBOUNDRY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDRY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDRY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nffbaz\r\n--MIMEBOUNDRY--\r\n",
},
{
`resource "template_cloudinit_config" "foo" {
gzip = true
base64_encode = false
part {
content_type = "text/x-shellscript"
content = "baz"
filename = "ah"
}
part {
content_type = "text/x-shellscript"
content = "ffbaz"
}
}`,
"\x1f\x8b\b\x00\x00\tn\x88\x00\xff\xac\xce\xc1J\x031\x10\xc6\xf1{`\xdf!\xe4>VO\u0096^\xb4=xX\x05\xa9\x82\xc7\xd9݉;\x90LB2\x85\xadOo-\x88\x8b\xe2\xadDŽ\x1f\xf3\xfd\xef\x93(\x89\xc2\xfe\x98\xa9\xb5\xf1\x10\x943\x16]E\x9ei\\\xdb>\x1dd\xc4rܸ\xee\xa1\xdb\xdd=\xbd<n\x9fߜ\xf9z\xc0+\x95\xcaIZ{su\xdd\x18\x80\x85h\xcc\xf7\xdd-ל*\xeb\x19\xa2*\x0eS<\xfd\xaf\xad\xe7@\x82\x916\x0e'\xf7\xe3\xf7\x05\xa5z*\xb0\x93!\x8d,ﭽ\xedY\x17\xe0\x1c\xaa4\xebj\x86:Q\bu(\x9cO\xa2\xe3H\xbf\xa2\x1a\xd3\xe3ǿm\x97\xde\xf2\xfe\xef\x1a@c>\x03\x00\x00\xff\xffmB\x8c\xeed\x01\x00\x00",
},
}
for _, tt := range testCases {
r.Test(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
r.TestStep{
Config: tt.ResourceBlock,
Check: r.ComposeTestCheckFunc(
r.TestCheckResourceAttr("template_cloudinit_config.foo", "rendered", tt.Expected),
),
},
},
})
}
}
func TestCloudConfig_update(t *testing.T) {
r.Test(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
r.TestStep{
Config: testCloudInitConfig_basic,
Check: r.ComposeTestCheckFunc(
r.TestCheckResourceAttr("template_cloudinit_config.config", "rendered", testCloudInitConfig_basic_expected),
),
},
r.TestStep{
Config: testCloudInitConfig_update,
Check: r.ComposeTestCheckFunc(
r.TestCheckResourceAttr("template_cloudinit_config.config", "rendered", testCloudInitConfig_update_expected),
),
},
},
})
}
var testCloudInitConfig_basic = `
resource "template_cloudinit_config" "config" {
part {
content_type = "text/x-shellscript"
content = "baz"
}
}`
var testCloudInitConfig_basic_expected = `Content-Type: multipart/mixed; boundary=\"MIMEBOUNDRY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDRY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDRY--\r\n`
var testCloudInitConfig_update = `
resource "template_cloudinit_config" "config" {
part {
content_type = "text/x-shellscript"
content = "baz"
}
part {
content_type = "text/x-shellscript"
content = "ffbaz"
}
}`
var testCloudInitConfig_update_expected = `Content-Type: multipart/mixed; boundary=\"MIMEBOUNDARY\"\nMIME-Version: 1.0\r\n--MIMEBOUNDARY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nbaz\r\n--MIMEBOUNDARY\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/x-shellscript\r\nMime-Version: 1.0\r\n\r\nffbaz\r\n--MIMEBOUNDARY--\r\n`

View File

@ -17,7 +17,7 @@ func TestState_basic(t *testing.T) {
Config: testAccState_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckStateValue(
"terraform_remote_state.foo", "foo", "bar"),
"data.terraform_remote_state.foo", "foo", "bar"),
),
},
},
@ -64,7 +64,7 @@ func testAccCheckStateValue(id, name, value string) resource.TestCheckFunc {
}
const testAccState_basic = `
resource "terraform_remote_state" "foo" {
data "terraform_remote_state" "foo" {
backend = "_local"
config {

View File

@ -1,7 +1,7 @@
---
layout: "template"
page_title: "Template: cloudinit_multipart"
sidebar_current: "docs-template-resource-cloudinit-config"
sidebar_current: "docs-template-datasource-cloudinit-config"
description: |-
Renders a multi-part cloud-init config from source files.
---
@ -14,7 +14,7 @@ Renders a multi-part cloud-init config from source files.
```
# Render a part using a `template_file`
resource "template_file" "script" {
data "template_file" "script" {
template = "${file("${path.module}/init.tpl")}"
vars {
@ -24,7 +24,7 @@ resource "template_file" "script" {
# Render a multi-part cloudinit config making use of the part
# above, and other source files
resource "template_cloudinit_config" "config" {
data "template_cloudinit_config" "config" {
gzip = true
base64_encode = true
@ -32,7 +32,7 @@ resource "template_cloudinit_config" "config" {
part {
filename = "init.cfg"
content_type = "text/part-handler"
content = "${template_file.script.rendered}"
content = "${data.template_file.script.rendered}"
}
part {
@ -50,7 +50,7 @@ resource "template_cloudinit_config" "config" {
resource "aws_instance" "web" {
ami = "ami-d05e75b8"
instance_type = "t2.micro"
user_data = "${template_cloudinit_config.config.rendered}"
user_data = "${data.template_cloudinit_config.config.rendered}"
}
```

View File

@ -1,7 +1,7 @@
---
layout: "template"
page_title: "Template: template_file"
sidebar_current: "docs-template-resource-file"
sidebar_current: "docs-template-datasource-file"
description: |-
Renders a template from a file.
---
@ -13,7 +13,7 @@ Renders a template from a file.
## Example Usage
```
resource "template_file" "init" {
data "template_file" "init" {
template = "${file("${path.module}/init.tpl")}"
vars {

View File

@ -8,23 +8,16 @@ description: |-
# Template Provider
The template provider exposes resources to use templates to generate
The template provider exposes data sources to use templates to generate
strings for other Terraform resources or outputs.
The template provider is what we call a _logical provider_. This has no
impact on how it behaves, but conceptually it is important to understand.
The template provider doesn't manage any _physical_ resources; it isn't
creating servers, writing files, etc. It is used to generate attributes that
can be used for interpolation for other resources. Examples will explain
this best.
Use the navigation to the left to read about the available resources.
Use the navigation to the left to read about the available data sources.
## Example Usage
```
# Template for initial configuration bash script
resource "template_file" "init" {
data "template_file" "init" {
template = "${file("init.tpl")}"
vars {
@ -36,6 +29,6 @@ resource "template_file" "init" {
resource "aws_instance" "web" {
# ...
user_data = "${template_file.init.rendered}"
user_data = "${data.template_file.init.rendered}"
}
```

View File

@ -10,14 +10,14 @@
<a href="/docs/providers/template/index.html">Template Provider</a>
</li>
<li<%= sidebar_current(/^docs-template-resource/) %>>
<a href="#">Resources</a>
<li<%= sidebar_current(/^docs-template-datasource/) %>>
<a href="#">Data Sources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-template-resource-file") %>>
<a href="/docs/providers/template/r/file.html">template_file</a>
<li<%= sidebar_current("docs-template-datasource-file") %>>
<a href="/docs/providers/template/d/file.html">template_file</a>
</li>
<li<%= sidebar_current("docs-template-resource-cloudinit-config") %>>
<a href="/docs/providers/template/r/cloudinit_config.html">template_cloudinit_config</a>
<li<%= sidebar_current("docs-template-datasource-cloudinit-config") %>>
<a href="/docs/providers/template/d/cloudinit_config.html">template_cloudinit_config</a>
</li>
</ul>
</li>