mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
Merge #13652: template_dir resource
This commit is contained in:
commit
2871641c8f
@ -20,6 +20,7 @@ func Provider() terraform.ResourceProvider {
|
||||
"template_cloudinit_config",
|
||||
dataSourceCloudinitConfig(),
|
||||
),
|
||||
"template_dir": resourceDir(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
234
builtin/providers/template/resource_template_dir.go
Normal file
234
builtin/providers/template/resource_template_dir.go
Normal file
@ -0,0 +1,234 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/pathorcontents"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
)
|
||||
|
||||
func resourceDir() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: resourceTemplateDirCreate,
|
||||
Read: resourceTemplateDirRead,
|
||||
Delete: resourceTemplateDirDelete,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"source_dir": {
|
||||
Type: schema.TypeString,
|
||||
Description: "Path to the directory where the files to template reside",
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
"vars": {
|
||||
Type: schema.TypeMap,
|
||||
Optional: true,
|
||||
Default: make(map[string]interface{}),
|
||||
Description: "Variables to substitute",
|
||||
ValidateFunc: validateVarsAttribute,
|
||||
ForceNew: true,
|
||||
},
|
||||
"destination_dir": {
|
||||
Type: schema.TypeString,
|
||||
Description: "Path to the directory where the templated files will be written",
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resourceTemplateDirRead(d *schema.ResourceData, meta interface{}) error {
|
||||
sourceDir := d.Get("source_dir").(string)
|
||||
destinationDir := d.Get("destination_dir").(string)
|
||||
|
||||
// If the output doesn't exist, mark the resource for creation.
|
||||
if _, err := os.Stat(destinationDir); os.IsNotExist(err) {
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the combined hash of the input and output directories is different from
|
||||
// the stored one, mark the resource for re-creation.
|
||||
//
|
||||
// The output directory is technically enough for the general case, but by
|
||||
// hashing the input directory as well, we make development much easier: when
|
||||
// a developer modifies one of the input files, the generation is
|
||||
// re-triggered.
|
||||
hash, err := generateID(sourceDir, destinationDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hash != d.Id() {
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceTemplateDirCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
sourceDir := d.Get("source_dir").(string)
|
||||
destinationDir := d.Get("destination_dir").(string)
|
||||
vars := d.Get("vars").(map[string]interface{})
|
||||
|
||||
// Always delete the output first, otherwise files that got deleted from the
|
||||
// input directory might still be present in the output afterwards.
|
||||
if err := resourceTemplateDirDelete(d, meta); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination directory and any other intermediate directories
|
||||
// leading to it.
|
||||
if _, err := os.Stat(destinationDir); err != nil {
|
||||
if err := os.MkdirAll(destinationDir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively crawl the input files/directories and generate the output ones.
|
||||
err := filepath.Walk(sourceDir, func(p string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, _ := filepath.Rel(sourceDir, p)
|
||||
return generateDirFile(p, path.Join(destinationDir, relPath), f, vars)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Compute ID.
|
||||
hash, err := generateID(sourceDir, destinationDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.SetId(hash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceTemplateDirDelete(d *schema.ResourceData, _ interface{}) error {
|
||||
d.SetId("")
|
||||
|
||||
destinationDir := d.Get("destination_dir").(string)
|
||||
if _, err := os.Stat(destinationDir); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(destinationDir); err != nil {
|
||||
return fmt.Errorf("could not delete directory %q: %s", destinationDir, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateDirFile(sourceDir, destinationDir string, f os.FileInfo, vars map[string]interface{}) error {
|
||||
inputContent, _, err := pathorcontents.Read(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputContent, err := execute(inputContent, vars)
|
||||
if err != nil {
|
||||
return templateRenderError(fmt.Errorf("failed to render %v: %v", sourceDir, err))
|
||||
}
|
||||
|
||||
outputDir := path.Dir(destinationDir)
|
||||
if _, err := os.Stat(outputDir); err != nil {
|
||||
if err := os.MkdirAll(outputDir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(destinationDir, []byte(outputContent), f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateID(sourceDir, destinationDir string) (string, error) {
|
||||
inputHash, err := generateDirHash(sourceDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
outputHash, err := generateDirHash(destinationDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
checksum := sha1.Sum([]byte(inputHash + outputHash))
|
||||
return hex.EncodeToString(checksum[:]), nil
|
||||
}
|
||||
|
||||
func generateDirHash(directoryPath string) (string, error) {
|
||||
tarData, err := tarDir(directoryPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not generate output checksum: %s", err)
|
||||
}
|
||||
|
||||
checksum := sha1.Sum(tarData)
|
||||
return hex.EncodeToString(checksum[:]), nil
|
||||
}
|
||||
|
||||
func tarDir(directoryPath string) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
tw := tar.NewWriter(buf)
|
||||
|
||||
writeFile := func(p string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var header *tar.Header
|
||||
var file *os.File
|
||||
|
||||
header, err = tar.FileInfoHeader(f, f.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, _ := filepath.Rel(directoryPath, p)
|
||||
header.Name = relPath
|
||||
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err = os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(tw, file)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := filepath.Walk(directoryPath, writeFile); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
if err := tw.Flush(); err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
104
builtin/providers/template/resource_template_dir_test.go
Normal file
104
builtin/providers/template/resource_template_dir_test.go
Normal file
@ -0,0 +1,104 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
r "github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const templateDirRenderingConfig = `
|
||||
resource "template_dir" "dir" {
|
||||
source_dir = "%s"
|
||||
destination_dir = "%s"
|
||||
vars = %s
|
||||
}`
|
||||
|
||||
type testTemplate struct {
|
||||
template string
|
||||
want string
|
||||
}
|
||||
|
||||
func testTemplateDirWriteFiles(files map[string]testTemplate) (in, out string, err error) {
|
||||
in, err = ioutil.TempDir(os.TempDir(), "terraform_template_dir")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for name, file := range files {
|
||||
path := filepath.Join(in, name)
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(path), 0777)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path, []byte(file.template), 0777)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
out = fmt.Sprintf("%s.out", in)
|
||||
return
|
||||
}
|
||||
|
||||
func TestTemplateDirRendering(t *testing.T) {
|
||||
var cases = []struct {
|
||||
vars string
|
||||
files map[string]testTemplate
|
||||
}{
|
||||
{
|
||||
files: map[string]testTemplate{
|
||||
"foo.txt": {"${bar}", "bar"},
|
||||
"nested/monkey.txt": {"ooh-ooh-ooh-eee-eee", "ooh-ooh-ooh-eee-eee"},
|
||||
"maths.txt": {"${1+2+3}", "6"},
|
||||
},
|
||||
vars: `{bar = "bar"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
// Write the desired templates in a temporary directory.
|
||||
in, out, err := testTemplateDirWriteFiles(tt.files)
|
||||
if err != nil {
|
||||
t.Skipf("could not write templates to temporary directory: %s", err)
|
||||
continue
|
||||
}
|
||||
defer os.RemoveAll(in)
|
||||
defer os.RemoveAll(out)
|
||||
|
||||
// Run test case.
|
||||
r.UnitTest(t, r.TestCase{
|
||||
Providers: testProviders,
|
||||
Steps: []r.TestStep{
|
||||
{
|
||||
Config: fmt.Sprintf(templateDirRenderingConfig, in, out, tt.vars),
|
||||
Check: func(s *terraform.State) error {
|
||||
for name, file := range tt.files {
|
||||
content, err := ioutil.ReadFile(filepath.Join(out, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("template:\n%s\nvars:\n%s\ngot:\n%s\nwant:\n%s\n", file.template, tt.vars, err, file.want)
|
||||
}
|
||||
if string(content) != file.want {
|
||||
return fmt.Errorf("template:\n%s\nvars:\n%s\ngot:\n%s\nwant:\n%s\n", file.template, tt.vars, content, file.want)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
CheckDestroy: func(*terraform.State) error {
|
||||
if _, err := os.Stat(out); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("template_dir did not get destroyed")
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
116
website/source/docs/providers/template/r/dir.html.md
Normal file
116
website/source/docs/providers/template/r/dir.html.md
Normal file
@ -0,0 +1,116 @@
|
||||
---
|
||||
layout: "template"
|
||||
page_title: "Template: template_dir"
|
||||
sidebar_current: "docs-template-resource-dir"
|
||||
description: |-
|
||||
Renders a directory of templates.
|
||||
---
|
||||
|
||||
# template_dir
|
||||
|
||||
Renders a directory containing templates into a separate directory of
|
||||
corresponding rendered files.
|
||||
|
||||
`template_dir` is similar to [`template_file`](../d/file.html) but it walks
|
||||
a given source directory and treats every file it encounters as a template,
|
||||
rendering it to a corresponding file in the destination directory.
|
||||
|
||||
~> **Note** When working with local files, Terraform will detect the resource
|
||||
as having been deleted each time a configuration is applied on a new machine
|
||||
where the destination dir is not present and will generate a diff to create
|
||||
it. This may cause "noise" in diffs in environments where configurations are
|
||||
routinely applied by many different users or within automation systems.
|
||||
|
||||
## Example Usage
|
||||
|
||||
The following example shows how one might use this resource to produce a
|
||||
directory of configuration files to upload to a compute instance, using
|
||||
Amazon EC2 as a placeholder.
|
||||
|
||||
```hcl
|
||||
resource "template_dir" "config" {
|
||||
source_dir = "${path.module}/instance_config_templates"
|
||||
destination_dir = "${path.cwd}/instance_config"
|
||||
|
||||
vars {
|
||||
consul_addr = "${var.consul_addr}"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_instance" "server" {
|
||||
ami = "${var.server_ami}"
|
||||
instance_type = "t2.micro"
|
||||
|
||||
connection {
|
||||
# ...connection configuration...
|
||||
}
|
||||
|
||||
provisioner "file" {
|
||||
# Referencing the template_dir resource ensures that it will be
|
||||
# created or updated before this aws_instance resource is provisioned.
|
||||
source = "${template_dir.config.destination_dir}"
|
||||
destination = "/etc/myapp"
|
||||
}
|
||||
}
|
||||
|
||||
variable "consul_addr" {}
|
||||
|
||||
variable "server_ami" {}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `source_dir` - (Required) Path to the directory where the files to template reside.
|
||||
|
||||
* `destination_dir` - (Required) Path to the directory where the templated files will be written.
|
||||
|
||||
* `vars` - (Optional) Variables for interpolation within the template. Note
|
||||
that variables must all be primitives. Direct references to lists or maps
|
||||
will cause a validation error.
|
||||
|
||||
Any required parent directories of `destination_dir` will be created
|
||||
automatically, and any pre-existing file or directory at that location will
|
||||
be deleted before template rendering begins.
|
||||
|
||||
After rendering this resource remembers the content of both the source and
|
||||
destination directories in the Terraform state, and will plan to recreate the
|
||||
output directory if any changes are detected during the plan phase.
|
||||
|
||||
Note that it is _not_ safe to use the `file` interpolation function to read
|
||||
files create by this resource, since that function can be evaluated before the
|
||||
destination directory has been created or updated. It *is* safe to use the
|
||||
generated files with resources that directly take filenames as arguments,
|
||||
as long as the path is constructed using the `destination_dir` attribute
|
||||
to create a dependency relationship with the `template_dir` resource.
|
||||
|
||||
## Template Syntax
|
||||
|
||||
The syntax of the template files is the same as
|
||||
[standard interpolation syntax](/docs/configuration/interpolation.html),
|
||||
but you only have access to the variables defined in the `vars` section.
|
||||
|
||||
To access interpolations that are normally available to Terraform
|
||||
configuration (such as other variables, resource attributes, module
|
||||
outputs, etc.) you can expose them via `vars` as shown below:
|
||||
|
||||
```hcl
|
||||
resource "template_dir" "init" {
|
||||
# ...
|
||||
|
||||
vars {
|
||||
foo = "${var.foo}"
|
||||
attr = "${aws_instance.foo.private_ip}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
This resource exports the following attributes:
|
||||
|
||||
* `destination_dir` - The destination directory given in configuration.
|
||||
Interpolate this attribute into other resource configurations to create
|
||||
a dependency to ensure that the destination directory is populated before
|
||||
another resource attempts to read it.
|
@ -21,6 +21,15 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-template-resource") %>>
|
||||
<a href="#">Resources</a>
|
||||
<ul class="nav nav-visible">
|
||||
<li<%= sidebar_current("docs-template-resource-dir") %>>
|
||||
<a href="/docs/providers/template/r/dir.html">template_dir</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
Loading…
Reference in New Issue
Block a user