├── test ├── src │ ├── subdir │ │ ├── style.css │ │ └── index.html.tmpl │ ├── static.txt │ └── templated.txt.tmpl ├── amazon-s3 │ ├── .gitignore │ └── test.tf └── local-only │ ├── .gitignore │ └── test.tf ├── versions.tf ├── CHANGELOG.md ├── outputs.tf ├── LICENSE ├── variables.tf ├── files.tf └── README.md /test/src/subdir/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/src/static.txt: -------------------------------------------------------------------------------- 1 | Hello ${world}! 2 | -------------------------------------------------------------------------------- /test/src/templated.txt.tmpl: -------------------------------------------------------------------------------- 1 | Hello ${name}! 2 | -------------------------------------------------------------------------------- /test/amazon-s3/.gitignore: -------------------------------------------------------------------------------- 1 | terraform.tfstate* 2 | .terraform/* 3 | -------------------------------------------------------------------------------- /test/local-only/.gitignore: -------------------------------------------------------------------------------- 1 | terraform.tfstate* 2 | .terraform/* 3 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.12.8" 3 | } 4 | -------------------------------------------------------------------------------- /test/src/subdir/index.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Greeting 4 | 5 | 6 | 7 |

Hello, ${name}!

8 | 9 | 10 | -------------------------------------------------------------------------------- /test/local-only/test.tf: -------------------------------------------------------------------------------- 1 | module "under_test" { 2 | source = "../../" 3 | 4 | base_dir = "${path.module}/../src" 5 | template_vars = { 6 | name = "Josephine" 7 | } 8 | } 9 | 10 | output "result" { 11 | value = module.under_test 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.0.2 2 | 3 | Bugs fixed: 4 | 5 | * The default Content-Type for the `.svg` suffix is now `image/svg+xml`, rather 6 | than just `image/svg`. ([#1](https://github.com/hashicorp/terraform-template-dir/pull/1)) 7 | 8 | # v1.0.1 9 | 10 | v1.0.1 contained only documentation changes relative to v1.0.0, to represent 11 | that the module moved into the "hashicorp" namespace on Terraform Registry. 12 | 13 | # v1.0.0 14 | 15 | Initial release 16 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "files" { 2 | value = local.files 3 | description = "Map from relative file paths to objects describing all of the files. See the module README for more information." 4 | } 5 | 6 | output "files_on_disk" { 7 | value = { for p, f in local.files : p => f if f.source_path != null } 8 | description = "A filtered version of the files output that includes only entries that point to static files on disk." 9 | } 10 | 11 | output "files_in_memory" { 12 | value = { for p, f in local.files : p => f if f.content != null } 13 | description = "A filtered version of the files output that includes only entries that have rendered content in memory." 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 HashiCorp, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/amazon-s3/test.tf: -------------------------------------------------------------------------------- 1 | module "template_files" { 2 | source = "../../" 3 | 4 | base_dir = "${path.module}/../src" 5 | template_vars = { 6 | name = "Josephine" 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "us-west-2" 12 | } 13 | 14 | resource "random_string" "suffix" { 15 | length = 8 16 | special = false 17 | upper = false 18 | } 19 | 20 | resource "aws_s3_bucket" "static_files" { 21 | bucket = "terraform-template-dir-test-${random_string.suffix.result}" 22 | } 23 | 24 | resource "aws_s3_bucket_object" "static_files" { 25 | for_each = module.template_files.files 26 | 27 | bucket = aws_s3_bucket.static_files.bucket 28 | key = each.key 29 | content_type = each.value.content_type 30 | 31 | # The template_files module guarantees that only one of these two attributes 32 | # will be set for each file, depending on whether it is an in-memory template 33 | # rendering result or a static file on disk. 34 | source = each.value.source_path 35 | content = each.value.content 36 | 37 | # Unless the bucket has encryption enabled, the ETag of each object is an 38 | # MD5 hash of that object. 39 | etag = each.value.digests.md5 40 | } 41 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "base_dir" { 2 | type = string 3 | description = "The base directory in which this module will search for static files and templates." 4 | } 5 | 6 | variable "template_vars" { 7 | type = any 8 | default = {} 9 | description = "Variables to make available for interpolation and other expressions in template files." 10 | } 11 | 12 | variable "template_file_suffix" { 13 | type = string 14 | default = ".tmpl" 15 | description = "The filename suffix that indicates that a file is a Terraform template file rather than a static file." 16 | } 17 | 18 | variable "file_types" { 19 | type = map(string) 20 | default = { 21 | ".txt" = "text/plain; charset=utf-8" 22 | ".html" = "text/html; charset=utf-8" 23 | ".htm" = "text/html; charset=utf-8" 24 | ".xhtml" = "application/xhtml+xml" 25 | ".css" = "text/css; charset=utf-8" 26 | ".js" = "application/javascript" 27 | ".xml" = "application/xml" 28 | ".json" = "application/json" 29 | ".jsonld" = "application/ld+json" 30 | ".gif" = "image/gif" 31 | ".jpeg" = "image/jpeg" 32 | ".jpg" = "image/jpeg" 33 | ".png" = "image/png" 34 | ".svg" = "image/svg+xml" 35 | ".webp" = "image/webp" 36 | ".weba" = "audio/webm" 37 | ".webm" = "video/webm" 38 | ".3gp" = "video/3gpp" 39 | ".3g2" = "video/3gpp2" 40 | ".pdf" = "application/pdf" 41 | ".swf" = "application/x-shockwave-flash" 42 | ".atom" = "application/atom+xml" 43 | ".rss" = "application/rss+xml" 44 | ".ico" = "image/vnd.microsoft.icon" 45 | ".jar" = "application/java-archive" 46 | ".ttf" = "font/ttf" 47 | ".otf" = "font/otf" 48 | ".eot" = "application/vnd.ms-fontobject" 49 | ".woff" = "font/woff" 50 | ".woff2" = "font/woff2" 51 | } 52 | description = "Map from file suffixes, which must begin with a period and contain no periods, to the corresponding Content-Type values." 53 | } 54 | 55 | variable "default_file_type" { 56 | type = string 57 | default = "application/octet-stream" 58 | description = "The Content-Type value to use for any files that don't match one of the suffixes given in file_types." 59 | } 60 | -------------------------------------------------------------------------------- /files.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | all_file_paths = fileset(var.base_dir, "**") 3 | static_file_paths = toset([ 4 | for p in local.all_file_paths : p 5 | if length(p) < length(var.template_file_suffix) || substr(p, length(p) - length(var.template_file_suffix), length(var.template_file_suffix)) != var.template_file_suffix 6 | ]) 7 | template_file_paths = { 8 | for p in local.all_file_paths : 9 | substr(p, 0, length(p) - length(var.template_file_suffix)) => p 10 | if ! contains(local.static_file_paths, p) 11 | } 12 | 13 | template_file_contents = { 14 | for p, sp in local.template_file_paths : p => templatefile("${var.base_dir}/${sp}", var.template_vars) 15 | } 16 | static_file_local_paths = { 17 | for p in local.static_file_paths : p => "${var.base_dir}/${p}" 18 | } 19 | 20 | output_file_paths = setunion(keys(local.template_file_paths), local.static_file_paths) 21 | 22 | file_suffix_matches = { 23 | for p in local.output_file_paths : p => regexall("\\.[^\\.]+\\z", p) 24 | } 25 | file_suffixes = { 26 | for p, ms in local.file_suffix_matches : p => length(ms) > 0 ? ms[0] : "" 27 | } 28 | file_types = { 29 | for p in local.output_file_paths : p => lookup(var.file_types, local.file_suffixes[p], var.default_file_type) 30 | } 31 | 32 | files = merge( 33 | { 34 | for p in keys(local.template_file_paths) : p => { 35 | content_type = local.file_types[p] 36 | source_path = tostring(null) 37 | content = local.template_file_contents[p] 38 | digests = tomap({ 39 | md5 = md5(local.template_file_contents[p]) 40 | sha1 = sha1(local.template_file_contents[p]) 41 | sha256 = sha256(local.template_file_contents[p]) 42 | sha512 = sha512(local.template_file_contents[p]) 43 | base64sha256 = base64sha256(local.template_file_contents[p]) 44 | base64sha512 = base64sha512(local.template_file_contents[p]) 45 | }) 46 | } 47 | }, 48 | { 49 | for p in local.static_file_paths : p => { 50 | content_type = local.file_types[p] 51 | source_path = local.static_file_local_paths[p] 52 | content = tostring(null) 53 | digests = tomap({ 54 | md5 = filemd5(local.static_file_local_paths[p]) 55 | sha1 = filesha1(local.static_file_local_paths[p]) 56 | sha256 = filesha256(local.static_file_local_paths[p]) 57 | sha512 = filesha512(local.static_file_local_paths[p]) 58 | base64sha256 = filebase64sha256(local.static_file_local_paths[p]) 59 | base64sha512 = filebase64sha512(local.static_file_local_paths[p]) 60 | }) 61 | } 62 | }, 63 | ) 64 | /*files = { 65 | for p in local.output_file_paths : p => { 66 | content_type = local.file_types[p] 67 | source_path = lookup(local.static_file_local_paths, p, null) 68 | content = lookup(local.template_file_contents, p, null) 69 | } 70 | }*/ 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Template Directory Module 2 | 3 | This is a compute-only Terraform module (that is, a module that doesn't make 4 | any remote API calls) which gathers all of the files under a particular 5 | base directory and renders those which have a particular suffix as Terraform 6 | template files. 7 | 8 | ```hcl 9 | module "template_files" { 10 | source = "hashicorp/dir/template" 11 | 12 | base_dir = "${path.module}/src" 13 | template_vars = { 14 | # Pass in any values that you wish to use in your templates. 15 | vpc_id = "vpc-abc123" 16 | } 17 | } 18 | ``` 19 | 20 | The `files` output is a map from file paths relative to the base directory 21 | to objects with the following attributes: 22 | 23 | * `content_type`: A MIME type to use for the file. 24 | * `content`: Literal content of the file, after rendering a template. 25 | * `source_path`: Local filesystem location of a non-template file. 26 | * `digests`: A map containing the results of applying various digest/hash 27 | algorithms to the file content. 28 | 29 | `content` and `source_path` are mutually exclusive. `content` is set for 30 | template files and contains the result of rendering the template. For 31 | non-template files, `source_path` is set to the location of the file on local 32 | disk, which avoids trying to load non-UTF-8 files such as images into memory. 33 | 34 | The `digests` map for each file contains the following keys, whose values are 35 | the result of applying the named hash function to the file contents: 36 | 37 | * `md5` 38 | * `sha1` 39 | * `sha256` 40 | * `base64sha256` 41 | * `base512` 42 | * `base64sha512` 43 | 44 | ## Template Files 45 | 46 | By default, any file in the base directory whose filename ends in `.tmpl` is 47 | interpreted as a template. You can override that suffix by setting the 48 | variable `template_file_suffix` to any string that starts with a period and 49 | is followed by one or more non-period characters. 50 | 51 | The templates are interpreted as 52 | [Terraform's string template syntax](https://www.terraform.io/docs/configuration/expressions.html#string-templates). Templates can use any of 53 | [Terraform's built-in functions](https://www.terraform.io/docs/configuration/functions.html) except 54 | [the `templatefile` function](https://www.terraform.io/docs/configuration/functions/templatefile.html), 55 | which is what this module uses for template rendering internally. 56 | 57 | Any file that does not have the template file suffix will be treated as a 58 | static file, returning the local path to the source file. 59 | 60 | ## Content-Type Mapping 61 | 62 | Content-Type values (`content_type` in the resulting objects) are selected 63 | based on the suffixes of all of the discovered files. 64 | 65 | The variable `file_types` is a mapping from filename suffixes (a dot followed 66 | by at least one non-dot character) to `Content-Type` header values. The default 67 | mapping includes a number of filetypes commonly used on static websites. 68 | 69 | If the module encounters a file that has no suffix at all or whose suffix is not 70 | in `file_types`, it will use the value of variable `default_file_type` as a 71 | fallback, which itself defaults to `application/octet-stream`. 72 | 73 | ## Uploading Files to Amazon S3 74 | 75 | A key use-case for this module is to produce content to upload into an Amazon S3 76 | bucket, for example to use as a static website. 77 | 78 | In your calling module, use 79 | [`aws_s3_bucket_object` from the AWS provider](https://www.terraform.io/docs/providers/aws/r/s3_bucket_object.html) 80 | with `for_each` to create an S3 object for each file: 81 | 82 | ```hcl 83 | resource "aws_s3_bucket_object" "static_files" { 84 | for_each = module.template_files.files 85 | 86 | bucket = "example" 87 | key = each.key 88 | content_type = each.value.content_type 89 | 90 | # The template_files module guarantees that only one of these two attributes 91 | # will be set for each file, depending on whether it is an in-memory template 92 | # rendering result or a static file on disk. 93 | source = each.value.source_path 94 | content = each.value.content 95 | 96 | # Unless the bucket has encryption enabled, the ETag of each object is an 97 | # MD5 hash of that object. 98 | etag = each.value.digests.md5 99 | } 100 | ``` 101 | 102 | ## Uploading files to Google Cloud Storage 103 | 104 | The pattern for uploading files to GCS is very similar to that for Amazon S3 105 | above: 106 | 107 | ```hcl 108 | resource "google_storage_bucket_object" "picture" { 109 | for_each = module.template_files.files 110 | 111 | bucket = "example" 112 | name = each.key 113 | content_type = each.value.content_type 114 | 115 | # The template_files module guarantees that only one of these two attributes 116 | # will be set for each file, depending on whether it is an in-memory template 117 | # rendering result or a static file on disk. 118 | source = each.value.source_path 119 | content = each.value.content 120 | } 121 | ``` 122 | 123 | ## Requirements 124 | 125 | This module requires Terraform v0.12.8 or later. It does not use any Terraform 126 | providers, and does not declare any Terraform resources. 127 | 128 | ## Why not use the `template_dir` resource type? 129 | 130 | The `template_dir` resource type was implemented as a pragmatic workaround for 131 | various limitations in earlier versions of Terraform, but it's problematic 132 | because it violates an assumption Terraform makes about resources: it 133 | modifies local state on the system where Terraform is running, and thus 134 | the result of the resource is not visible when running Terraform on other 135 | hosts. 136 | 137 | The `template_dir` resource type is no longer necessary from Terraform 0.12.8 138 | onwards for most use-cases, because there's enough built-in functionality to 139 | get similar results with no resources at all. 140 | 141 | As well as this module being a better citizen in Terraform's workflow than 142 | a `template_file` resource, it also allows a mixture of template and 143 | non-template files in the same directory, and will only load into memory 144 | and render the template files. For non-template files, it will just leave 145 | them on disk where they are and return a local filesystem path to the original 146 | location. 147 | 148 | On the other hand, this module _does_ assume that its result will be used with 149 | some other resource type that is able to deal with some files being rendered 150 | strings in memory and other files being read directly from disk. This is true 151 | for `aws_s3_bucket_object`, but not true for all resource types that might 152 | work with arbitrary files. 153 | --------------------------------------------------------------------------------