mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-08 15:13:56 -06:00
eaac9fbca3
Previously we were letting it get implicitly created as part of making the structure for copying in each file, but that isn't sufficient if the source directory is empty. By explicitly creating the directory first we ensure that it will complete successfully even in the case of an empty directory.
235 lines
5.5 KiB
Go
235 lines
5.5 KiB
Go
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
|
|
}
|