├── .github ├── .github-update-disabled ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── banner.png ├── mergify.yml ├── renovate.json ├── settings.yml └── workflows │ ├── branch.yml │ ├── chatops.yml │ ├── release.yml │ └── scheduled.yml ├── .gitignore ├── LICENSE ├── README.md ├── README.yaml ├── atmos.yaml ├── descriptors.tf ├── examples ├── autoscalinggroup │ ├── .gitignore │ ├── autoscalinggroup.auto.tfvars │ ├── context.tf │ ├── main.tf │ ├── outputs.tf │ └── versions.tf └── complete │ ├── .gitignore │ ├── compatibility.tf │ ├── complete.auto.tfvars │ ├── context.tf │ ├── descriptors.tf │ ├── label1.tf │ ├── label1t1.tf │ ├── label1t2.tf │ ├── label2.tf │ ├── label3c.tf │ ├── label3n.tf │ ├── label4.tf │ ├── label5.tf │ ├── label6f.tf │ ├── label6t.tf │ ├── label7.tf │ ├── label8d.tf │ ├── label8dcd.tf │ ├── label8dnd.tf │ ├── label8l.tf │ ├── label8n.tf │ ├── label8t.tf │ ├── label8u.tf │ ├── module │ └── compare │ │ └── compare.tf │ └── versions.tf ├── exports └── context.tf ├── main.tf ├── outputs.tf ├── test ├── .gitignore ├── Makefile ├── Makefile.alpine └── src │ ├── .gitignore │ ├── Makefile │ ├── examples_complete_test.go │ ├── go.mod │ └── go.sum ├── variables.tf └── versions.tf /.github/.github-update-disabled: -------------------------------------------------------------------------------- 1 | This presence of a .github/.github-update-disabled file 2 | prevents `make github/update` from making any changes. 3 | The contents of the file are ignored. 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Use this file to define individuals or teams that are responsible for code in a repository. 2 | # Read more: 3 | # 4 | # Order is important: the last matching pattern has the highest precedence 5 | 6 | # These owners will be the default owners for everything 7 | * @cloudposse/engineering @cloudposse/contributors 8 | 9 | # Cloud Posse must review any changes to Makefiles 10 | **/Makefile @cloudposse/engineering 11 | **/Makefile.* @cloudposse/engineering 12 | 13 | # Cloud Posse must review any changes to GitHub actions 14 | .github/* @cloudposse/engineering 15 | 16 | # Cloud Posse must review any changes to standard context definition, 17 | # but some changes can be rubber-stamped. 18 | **/*.tf @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers 19 | README.yaml @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers 20 | README.md @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers 21 | docs/*.md @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers 22 | 23 | # Cloud Posse Admins must review all changes to CODEOWNERS or the mergify configuration 24 | .github/mergify.yml @cloudposse/admins 25 | .github/CODEOWNERS @cloudposse/admins 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Found a bug? Maybe our [Slack Community](https://slack.cloudposse.com) can help. 11 | 12 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) 13 | 14 | ## Describe the Bug 15 | A clear and concise description of what the bug is. 16 | 17 | ## Expected Behavior 18 | A clear and concise description of what you expected to happen. 19 | 20 | ## Steps to Reproduce 21 | Steps to reproduce the behavior: 22 | 1. Go to '...' 23 | 2. Run '....' 24 | 3. Enter '....' 25 | 4. See error 26 | 27 | ## Screenshots 28 | If applicable, add screenshots or logs to help explain your problem. 29 | 30 | ## Environment (please complete the following information): 31 | 32 | Anything that will help us triage the bug will help. Here are some ideas: 33 | - OS: [e.g. Linux, OSX, WSL, etc] 34 | - Version [e.g. 10.15] 35 | 36 | ## Additional Context 37 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | 5 | - name: Community Slack Team 6 | url: https://cloudposse.com/slack/ 7 | about: |- 8 | Please ask and answer questions here. 9 | 10 | - name: Office Hours 11 | url: https://cloudposse.com/office-hours/ 12 | about: |- 13 | Join us every Wednesday for FREE Office Hours (lunch & learn). 14 | 15 | - name: DevOps Accelerator Program 16 | url: https://cloudposse.com/accelerate/ 17 | about: |- 18 | Own your infrastructure in record time. We build it. You drive it. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'feature request' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Have a question? Please checkout our [Slack Community](https://slack.cloudposse.com) or visit our [Slack Archive](https://archive.sweetops.com/). 11 | 12 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) 13 | 14 | ## Describe the Feature 15 | 16 | A clear and concise description of what the bug is. 17 | 18 | ## Expected Behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## Use Case 23 | 24 | Is your feature request related to a problem/challenge you are trying to solve? Please provide some additional context of why this feature or capability will be valuable. 25 | 26 | ## Describe Ideal Solution 27 | 28 | A clear and concise description of what you want to happen. If you don't know, that's okay. 29 | 30 | ## Alternatives Considered 31 | 32 | Explain what alternative solutions or features you've considered. 33 | 34 | ## Additional Context 35 | 36 | Add any other context or screenshots about the feature request here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse/terraform-null-label/52e26ac583739dd012215389b61c413f4df45dd5/.github/ISSUE_TEMPLATE/question.md -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## what 2 | * Describe high-level what changed as a result of these commits (i.e. in plain-english, what do these changes mean?) 3 | * Use bullet points to be concise and to the point. 4 | 5 | ## why 6 | * Provide the justifications for the changes (e.g. business case). 7 | * Describe why these changes were made (e.g. why do these commits fix the problem?) 8 | * Use bullet points to be concise and to the point. 9 | 10 | ## references 11 | * Link to any supporting github issues or helpful documentation to add some context (e.g. stackoverflow). 12 | * Use `closes #123`, if this PR closes a GitHub issue `#123` 13 | 14 | -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse/terraform-null-label/52e26ac583739dd012215389b61c413f4df45dd5/.github/banner.png -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | extends: .github 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges" 5 | ], 6 | "labels": ["auto-update"], 7 | "dependencyDashboardAutoclose": true, 8 | "enabledManagers": ["terraform"], 9 | "terraform": { 10 | "ignorePaths": ["**/context.tf", "examples/**"] 11 | }, 12 | "postUpgradeTasks": { 13 | "commands": ["make readme"], 14 | "fileFilters": ["README.md"], 15 | "executionMode": "update" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # Upstream changes from _extends are only recognized when modifications are made to this file in the default branch. 2 | _extends: .github 3 | repository: 4 | name: terraform-null-label 5 | description: Terraform Module to define a consistent naming convention by (namespace, stage, name, [attributes]) 6 | homepage: https://cloudposse.com/accelerate 7 | topics: terraform, terraform-modules, naming-convention, name, namespace, stage, hcl2, labels, conventions 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/branch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Branch 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - release/** 8 | types: [opened, synchronize, reopened, labeled, unlabeled] 9 | push: 10 | branches: 11 | - main 12 | - release/v* 13 | paths-ignore: 14 | - '.github/**' 15 | - 'docs/**' 16 | - 'examples/**' 17 | - 'test/**' 18 | - 'README.md' 19 | 20 | permissions: {} 21 | 22 | jobs: 23 | terraform-module: 24 | uses: cloudposse/.github/.github/workflows/shared-terraform-module.yml@main 25 | secrets: inherit 26 | -------------------------------------------------------------------------------- /.github/workflows/chatops.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: chatops 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | permissions: 8 | pull-requests: write 9 | id-token: write 10 | contents: write 11 | statuses: write 12 | 13 | jobs: 14 | test: 15 | uses: cloudposse/.github/.github/workflows/shared-terraform-chatops.yml@main 16 | if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/terratest') }} 17 | secrets: inherit 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | permissions: 9 | id-token: write 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | terraform-module: 15 | uses: cloudposse/.github/.github/workflows/shared-release-branches.yml@main 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: scheduled 3 | on: 4 | workflow_dispatch: { } # Allows manually trigger this workflow 5 | schedule: 6 | - cron: "0 3 * * *" 7 | 8 | permissions: 9 | pull-requests: write 10 | id-token: write 11 | contents: write 12 | 13 | jobs: 14 | scheduled: 15 | uses: cloudposse/.github/.github/workflows/shared-terraform-scheduled.yml@main 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | **/.terraform.lock.hcl 8 | 9 | # .tfvars files 10 | *.tfvars 11 | 12 | **/.idea 13 | **/*.iml 14 | 15 | **/.build-harness 16 | **/build-harness 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017-2020 Cloud Posse, LLC 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Project Banner
5 | 6 | 7 |

Latest ReleaseLast UpdatedSlack CommunityGet Support 8 | 9 |

10 | 11 | 12 | 32 | 33 | Terraform module designed to generate consistent names and tags for resources. Use `terraform-null-label` to implement a strict naming convention. 34 | 35 | There are 6 inputs considered "labels" or "ID elements" (because the labels are used to construct the ID): 36 | 1. namespace 37 | 1. tenant 38 | 1. environment 39 | 1. stage 40 | 1. name 41 | 1. attributes 42 | 43 | This module generates IDs using the following convention by default: `{namespace}-{environment}-{stage}-{name}-{attributes}`. 44 | However, it is highly configurable. The delimiter (e.g. `-`) is configurable. Each label item is optional (although you must provide at least one). 45 | So if you prefer the term `stage` to `environment` and do not need `tenant`, you can exclude them 46 | and the label `id` will look like `{namespace}-{stage}-{name}-{attributes}`. 47 | - The `tenant` label was introduced in v0.25.0. To preserve backward compatibility, it is not included by default. 48 | - The `attributes` input is actually a list of strings and `{attributes}` expands to the list elements joined by the delimiter. 49 | - If `attributes` is excluded but `namespace`, `stage`, and `environment` are included, `id` will look like `{namespace}-{environment}-{stage}-{name}`. 50 | Excluding `attributes` is discouraged, though, because attributes are the main way modules modify the ID to ensure uniqueness when provisioning the same resource types. 51 | - If you want the label items in a different order, you can specify that, too, with the `label_order` list. 52 | - You can set a maximum length for the `id`, and the module will create a (probably) unique name that fits within that length. 53 | (The module uses a portion of the MD5 hash of the full `id` to represent the missing part, so there remains a slight chance of name collision.) 54 | - You can control the letter case of the generated labels which make up the `id` using `var.label_value_case`. 55 | - By default, all of the non-empty labels are also exported as tags, whether they appear in the `id` or not. 56 | You can control which labels are exported as tags by setting `labels_as_tags` to the list of labels you want exported, 57 | or the empty list `[]` if you want no labels exported as tags at all. Tags passed in via the `tags` variable are 58 | always exported, and regardless of settings, empty labels are never exported as tags. 59 | You can control the case of the tag names (keys) for the labels using `var.label_key_case`. 60 | Unlike the tags generated from the label inputs, tags passed in via the `tags` input are not modified. 61 | 62 | There is an unfortunate collision over the use of the key `name`. Cloud Posse uses `name` in this module 63 | to represent the component, such as `eks` or `rds`. AWS uses a tag with the key `Name` to store the full human-friendly 64 | identifier of the thing tagged, which this module outputs as `id`, not `name`. So when converting input labels 65 | to tags, the value of the `Name` key is set to the module `id` output, and there is no tag corresponding to the 66 | module `name` output. An empty `name` label will not prevent the `Name` tag from being exported. 67 | 68 | It's recommended to use one `terraform-null-label` module for every unique resource of a given resource type. 69 | For example, if you have 10 instances, there should be 10 different labels. 70 | However, if you have multiple different kinds of resources (e.g. instances, security groups, file systems, and elastic ips), then they can all share the same label assuming they are logically related. 71 | 72 | For most purposes, the `id` output is sufficient to create an ID or label for a resource, and if you want a different 73 | ID or a different format, you would instantiate another instance of `null-label` and configure it accordingly. However, 74 | to accomodate situations where you want all the same inputs to generate multiple descriptors, this module provides 75 | the `descriptors` output, which is a map of strings generated according to the format specified by the 76 | `descriptor_formats` input. This feature is intentionally simple and minimally configurable and will not be 77 | enhanced to add more features that are already in `null-label`. See [examples/complete/descriptors.tf](examples/complete/descriptors.tf) for examples. 78 | 79 | All [Cloud Posse Terraform modules](https://github.com/cloudposse?utf8=%E2%9C%93&q=terraform-&type=&language=) use this module to ensure resources can be instantiated multiple times within an account and without conflict. 80 | 81 | The Cloud Posse convention is to use labels as follows: 82 | - `namespace`: A short (3-4 letters) abbreviation of the company name, to ensure globally unique IDs for things like S3 buckets 83 | - `tenant`: _(Rarely needed)_ When a company creates a dedicated resource per customer, `tenant` can be used to identify the customer the resource is dedicated to 84 | - `environment`: A [short abbreviation](https://github.com/cloudposse/terraform-aws-utils/#introduction) for the AWS region hosting the resource, or `gbl` for resources like IAM roles that have no region 85 | - `stage`: The name or role of the account the resource is for, such as `prod` or `dev` 86 | - `name`: The name of the component that owns the resources, such as `eks` or `rds` 87 | 88 | **NOTE:** The `null` originally referred to the primary Terraform [provider](https://www.terraform.io/docs/providers/null/index.html) used in this module. 89 | With Terraform 0.12, this module no longer needs any provider, but the name was kept for continuity. 90 | 91 | - Releases of this module from `0.23.0` onward only work with Terraform 0.13 or newer. 92 | - Releases of this module from `0.12.0` through `0.22.1` support `HCL2` and are compatible with Terraform 0.12 or newer. 93 | - Releases of this module prior to `0.12.0` are compatible with earlier versions of terraform like Terraform 0.11. 94 | 95 | 96 | > [!TIP] 97 | > #### 👽 Use Atmos with Terraform 98 | > Cloud Posse uses [`atmos`](https://atmos.tools) to easily orchestrate multiple environments using Terraform.
99 | > Works with [Github Actions](https://atmos.tools/integrations/github-actions/), [Atlantis](https://atmos.tools/integrations/atlantis), or [Spacelift](https://atmos.tools/integrations/spacelift). 100 | > 101 | >
102 | > Watch demo of using Atmos with Terraform 103 | >
104 | > Example of running atmos to manage infrastructure from our Quick Start tutorial. 105 | > 106 | 107 | 108 | 109 | 110 | 111 | ## Usage 112 | 113 | ### Defaults 114 | 115 | Cloud Posse Terraform modules share a common `context` object that is meant to be passed from module to module. 116 | The context object is a single object that contains all the input values for `terraform-null-label`. 117 | However, each input value can also be specified individually by name as a standard Terraform variable, 118 | and the value of those variables, when set to something other than `null`, will override the value 119 | in the context object. In order to allow chaining of these objects, where the context object input to one 120 | module is transformed and passed on to the next module, all the variables default to `null` or empty collections. 121 | The actual default values used when nothing is explicitly set are described in the documentation below. 122 | 123 | For example, the default value of `delimiter` is shown as `null`, but if you leave it set to `null`, 124 | `terraform-null-label` will actually use the default delimiter `-` (hyphen). 125 | 126 | A non-obvious but intentional consequence of this design is that once a module sets a non-default value, 127 | future modules in the chain cannot reset the value back to the original default. Instead, the new setting 128 | becomes the new default for downstream modules. Also, collections are not overwritten, they are merged, 129 | so once a tag is added, it will remain in the tag set and cannot be removed, although its value can 130 | be overwritten. 131 | 132 | Because the purpose of `labels_as_tags` is primarily to prevent tags from being generated 133 | that would [conflict with the AWS provider's `default_tags`](https://github.com/hashicorp/terraform-provider-aws/issues/19204), it is an exception to the 134 | rule that variables override the setting in the context object. The value in the context 135 | object cannot be changed, so that later modules cannot re-enable a problematic tag. 136 | 137 | ### Simple Example 138 | 139 | ```hcl 140 | module "eg_prod_bastion_label" { 141 | source = "cloudposse/label/null" 142 | # Cloud Posse recommends pinning every module to a specific version 143 | # version = "x.x.x" 144 | 145 | namespace = "eg" 146 | stage = "prod" 147 | name = "bastion" 148 | attributes = ["public"] 149 | delimiter = "-" 150 | 151 | tags = { 152 | "BusinessUnit" = "XYZ", 153 | "Snapshot" = "true" 154 | } 155 | } 156 | ``` 157 | 158 | This will create an `id` with the value of `eg-prod-bastion-public` because when generating `id`, the default order is `namespace`, `environment`, `stage`, `name`, `attributes` 159 | (you can override it by using the `label_order` variable, see [Advanced Example 3](#advanced-example-3)). 160 | 161 | Now reference the label when creating an instance: 162 | 163 | ```hcl 164 | resource "aws_instance" "eg_prod_bastion_public" { 165 | instance_type = "t1.micro" 166 | tags = module.eg_prod_bastion_label.tags 167 | } 168 | ``` 169 | 170 | Or define a security group: 171 | 172 | ```hcl 173 | resource "aws_security_group" "eg_prod_bastion_public" { 174 | vpc_id = var.vpc_id 175 | name = module.eg_prod_bastion_label.id 176 | tags = module.eg_prod_bastion_label.tags 177 | egress { 178 | from_port = 0 179 | to_port = 0 180 | protocol = "-1" 181 | cidr_blocks = ["0.0.0.0/0"] 182 | } 183 | } 184 | ``` 185 | 186 | 187 | ### Advanced Example 188 | 189 | Here is a more complex example with two instances using two different labels. Note how efficiently the tags are defined for both the instance and the security group. 190 | 191 |
Click to show 192 | 193 | ```hcl 194 | module "eg_prod_bastion_label" { 195 | source = "cloudposse/label/null" 196 | # Cloud Posse recommends pinning every module to a specific version 197 | # version = "x.x.x" 198 | 199 | namespace = "eg" 200 | stage = "prod" 201 | name = "bastion" 202 | delimiter = "-" 203 | 204 | tags = { 205 | "BusinessUnit" = "XYZ", 206 | "Snapshot" = "true" 207 | } 208 | } 209 | 210 | module "eg_prod_bastion_abc_label" { 211 | source = "cloudposse/label/null" 212 | # Cloud Posse recommends pinning every module to a specific version 213 | # version = "x.x.x" 214 | 215 | attributes = ["abc"] 216 | 217 | tags = { 218 | "BusinessUnit" = "ABC" # Override the Business Unit tag set in the base label 219 | } 220 | 221 | # Copy all other fields from the base label 222 | context = module.eg_prod_bastion_label.context 223 | } 224 | 225 | resource "aws_security_group" "eg_prod_bastion_abc" { 226 | name = module.eg_prod_bastion_abc_label.id 227 | tags = module.eg_prod_bastion_abc_label.tags 228 | ingress { 229 | from_port = 22 230 | to_port = 22 231 | protocol = "tcp" 232 | cidr_blocks = ["0.0.0.0/0"] 233 | } 234 | } 235 | 236 | resource "aws_instance" "eg_prod_bastion_abc" { 237 | instance_type = "t1.micro" 238 | tags = module.eg_prod_bastion_abc_label.tags 239 | vpc_security_group_ids = [aws_security_group.eg_prod_bastion_abc.id] 240 | } 241 | 242 | module "eg_prod_bastion_xyz_label" { 243 | source = "cloudposse/label/null" 244 | # Cloud Posse recommends pinning every module to a specific version 245 | # version = "x.x.x" 246 | 247 | attributes = ["xyz"] 248 | 249 | context = module.eg_prod_bastion_label.context 250 | } 251 | 252 | resource "aws_security_group" "eg_prod_bastion_xyz" { 253 | name = module.eg_prod_bastion_xyz_label.id 254 | tags = module.eg_prod_bastion_xyz_label.tags 255 | ingress { 256 | from_port = 22 257 | to_port = 22 258 | protocol = "tcp" 259 | cidr_blocks = ["0.0.0.0/0"] 260 | } 261 | } 262 | 263 | resource "aws_instance" "eg_prod_bastion_xyz" { 264 | instance_type = "t1.micro" 265 | tags = module.eg_prod_bastion_xyz_label.tags 266 | vpc_security_group_ids = [aws_security_group.eg_prod_bastion_xyz.id] 267 | } 268 | ``` 269 | 270 |
271 | 272 | ### Advanced Example 2 273 | 274 | Here is a more complex example with an autoscaling group that has a different tagging schema than other resources and 275 | requires its tags to be in this format, which this module can generate via `additional_tag_map` and `tags_as_list_of_maps`: 276 | 277 |
Click to show 278 | 279 | ```hcl 280 | tags = [ 281 | { 282 | key = "Name", 283 | propagate_at_launch = true, 284 | value = "namespace-stage-name" 285 | }, 286 | { 287 | key = "Namespace", 288 | propagate_at_launch = true, 289 | value = "namespace" 290 | }, 291 | { 292 | key = "Stage", 293 | propagate_at_launch = true, 294 | value = "stage" 295 | } 296 | ] 297 | ``` 298 | 299 | Autoscaling group using propagating tagging below (full example: [autoscalinggroup](examples/autoscalinggroup/main.tf)) 300 | 301 | ```hcl 302 | ################################ 303 | # terraform-null-label example # 304 | ################################ 305 | module "label" { 306 | source = "../../" 307 | namespace = "cp" 308 | stage = "prod" 309 | name = "app" 310 | 311 | tags = { 312 | BusinessUnit = "Finance" 313 | ManagedBy = "Terraform" 314 | } 315 | 316 | additional_tag_map = { 317 | propagate_at_launch = true 318 | } 319 | } 320 | 321 | ####################### 322 | # Launch template # 323 | ####################### 324 | resource "aws_launch_template" "default" { 325 | # terraform-null-label example used here: Set template name prefix 326 | name_prefix = "${module.label.id}-" 327 | image_id = data.aws_ami.amazon_linux.id 328 | instance_type = "t2.micro" 329 | instance_initiated_shutdown_behavior = "terminate" 330 | 331 | vpc_security_group_ids = [data.aws_security_group.default.id] 332 | 333 | monitoring { 334 | enabled = false 335 | } 336 | # terraform-null-label example used here: Set tags on volumes 337 | tag_specifications { 338 | resource_type = "volume" 339 | tags = module.label.tags 340 | } 341 | } 342 | 343 | ###################### 344 | # Autoscaling group # 345 | ###################### 346 | resource "aws_autoscaling_group" "default" { 347 | # terraform-null-label example used here: Set ASG name prefix 348 | name_prefix = "${module.label.id}-" 349 | vpc_zone_identifier = data.aws_subnet_ids.all.ids 350 | max_size = 1 351 | min_size = 1 352 | desired_capacity = 1 353 | 354 | launch_template = { 355 | id = aws_launch_template.default.id 356 | version = "$$Latest" 357 | } 358 | 359 | # terraform-null-label example used here: Set tags on ASG and EC2 Servers 360 | tags = module.label.tags_as_list_of_maps 361 | } 362 | ``` 363 | 364 |
365 | 366 | ### Advanced Example 3 367 | 368 | See [complete example](./examples/complete) for even more examples. 369 | 370 | This example shows how you can pass the `context` output of one label module to the next label_module, 371 | allowing you to create one label that has the base set of values, and then creating every extra label 372 | as a derivative of that. 373 | 374 |
Click to show 375 | 376 | ```hcl 377 | module "label1" { 378 | source = "cloudposse/label/null" 379 | # Cloud Posse recommends pinning every module to a specific version 380 | # version = "x.x.x" 381 | 382 | namespace = "CloudPosse" 383 | tenant = "H.R.H" 384 | environment = "UAT" 385 | stage = "build" 386 | name = "Winston Churchroom" 387 | attributes = ["fire", "water", "earth", "air"] 388 | 389 | label_order = ["name", "tenant", "environment", "stage", "attributes"] 390 | 391 | tags = { 392 | "City" = "Dublin" 393 | "Environment" = "Private" 394 | } 395 | } 396 | 397 | module "label2" { 398 | source = "cloudposse/label/null" 399 | # Cloud Posse recommends pinning every module to a specific version 400 | # version = "x.x.x" 401 | 402 | name = "Charlie" 403 | tenant = "" # setting to `null` would have no effect 404 | stage = "test" 405 | delimiter = "+" 406 | regex_replace_chars = "/[^a-zA-Z0-9-+]/" 407 | 408 | additional_tag_map = { 409 | propagate_at_launch = true 410 | additional_tag = "yes" 411 | } 412 | 413 | tags = { 414 | "City" = "London" 415 | "Environment" = "Public" 416 | } 417 | 418 | context = module.label1.context 419 | } 420 | 421 | module "label3" { 422 | source = "cloudposse/label/null" 423 | # Cloud Posse recommends pinning every module to a specific version 424 | # version = "x.x.x" 425 | 426 | name = "Starfish" 427 | stage = "release" 428 | delimiter = "." 429 | regex_replace_chars = "/[^-a-zA-Z0-9.]/" 430 | 431 | tags = { 432 | "Eat" = "Carrot" 433 | "Animal" = "Rabbit" 434 | } 435 | 436 | context = module.label1.context 437 | } 438 | ``` 439 | 440 | This creates label outputs like this: 441 | 442 | ```hcl 443 | label1 = { 444 | "attributes" = tolist([ 445 | "fire", 446 | "water", 447 | "earth", 448 | "air", 449 | ]) 450 | "delimiter" = "-" 451 | "id" = "winstonchurchroom-hrh-uat-build-fire-water-earth-air" 452 | "name" = "winstonchurchroom" 453 | "namespace" = "cloudposse" 454 | "stage" = "build" 455 | "tenant" = "hrh" 456 | } 457 | label1_context = { 458 | "additional_tag_map" = {} 459 | "attributes" = tolist([ 460 | "fire", 461 | "water", 462 | "earth", 463 | "air", 464 | ]) 465 | "delimiter" = tostring(null) 466 | "enabled" = true 467 | "environment" = "UAT" 468 | "id_length_limit" = tonumber(null) 469 | "label_key_case" = tostring(null) 470 | "label_order" = tolist([ 471 | "name", 472 | "tenant", 473 | "environment", 474 | "stage", 475 | "attributes", 476 | ]) 477 | "label_value_case" = tostring(null) 478 | "name" = "Winston Churchroom" 479 | "namespace" = "CloudPosse" 480 | "regex_replace_chars" = tostring(null) 481 | "stage" = "build" 482 | "tags" = { 483 | "City" = "Dublin" 484 | "Environment" = "Private" 485 | } 486 | "tenant" = "H.R.H" 487 | } 488 | label1_normalized_context = { 489 | "additional_tag_map" = {} 490 | "attributes" = tolist([ 491 | "fire", 492 | "water", 493 | "earth", 494 | "air", 495 | ]) 496 | "delimiter" = "-" 497 | "enabled" = true 498 | "environment" = "uat" 499 | "id_length_limit" = 0 500 | "label_key_case" = "title" 501 | "label_order" = tolist([ 502 | "name", 503 | "tenant", 504 | "environment", 505 | "stage", 506 | "attributes", 507 | ]) 508 | "label_value_case" = "lower" 509 | "name" = "winstonchurchroom" 510 | "namespace" = "cloudposse" 511 | "regex_replace_chars" = "/[^-a-zA-Z0-9]/" 512 | "stage" = "build" 513 | "tags" = { 514 | "Attributes" = "fire-water-earth-air" 515 | "City" = "Dublin" 516 | "Environment" = "Private" 517 | "Name" = "winstonchurchroom-hrh-uat-build-fire-water-earth-air" 518 | "Namespace" = "cloudposse" 519 | "Stage" = "build" 520 | "Tenant" = "hrh" 521 | } 522 | "tenant" = "hrh" 523 | } 524 | label1_tags = tomap({ 525 | "Attributes" = "fire-water-earth-air" 526 | "City" = "Dublin" 527 | "Environment" = "Private" 528 | "Name" = "winstonchurchroom-hrh-uat-build-fire-water-earth-air" 529 | "Namespace" = "cloudposse" 530 | "Stage" = "build" 531 | "Tenant" = "hrh" 532 | }) 533 | label2 = { 534 | "attributes" = tolist([ 535 | "fire", 536 | "water", 537 | "earth", 538 | "air", 539 | ]) 540 | "delimiter" = "+" 541 | "id" = "charlie+uat+test+fire+water+earth+air" 542 | "name" = "charlie" 543 | "namespace" = "cloudposse" 544 | "stage" = "test" 545 | "tenant" = "" 546 | } 547 | label2_context = { 548 | "additional_tag_map" = { 549 | "additional_tag" = "yes" 550 | "propagate_at_launch" = "true" 551 | } 552 | "attributes" = tolist([ 553 | "fire", 554 | "water", 555 | "earth", 556 | "air", 557 | ]) 558 | "delimiter" = "+" 559 | "enabled" = true 560 | "environment" = "UAT" 561 | "id_length_limit" = tonumber(null) 562 | "label_key_case" = tostring(null) 563 | "label_order" = tolist([ 564 | "name", 565 | "tenant", 566 | "environment", 567 | "stage", 568 | "attributes", 569 | ]) 570 | "label_value_case" = tostring(null) 571 | "name" = "Charlie" 572 | "namespace" = "CloudPosse" 573 | "regex_replace_chars" = "/[^a-zA-Z0-9-+]/" 574 | "stage" = "test" 575 | "tags" = { 576 | "City" = "London" 577 | "Environment" = "Public" 578 | } 579 | "tenant" = "" 580 | } 581 | label2_tags = tomap({ 582 | "Attributes" = "fire+water+earth+air" 583 | "City" = "London" 584 | "Environment" = "Public" 585 | "Name" = "charlie+uat+test+fire+water+earth+air" 586 | "Namespace" = "cloudposse" 587 | "Stage" = "test" 588 | }) 589 | label2_tags_as_list_of_maps = [ 590 | { 591 | "additional_tag" = "yes" 592 | "key" = "Attributes" 593 | "propagate_at_launch" = "true" 594 | "value" = "fire+water+earth+air" 595 | }, 596 | { 597 | "additional_tag" = "yes" 598 | "key" = "City" 599 | "propagate_at_launch" = "true" 600 | "value" = "London" 601 | }, 602 | { 603 | "additional_tag" = "yes" 604 | "key" = "Environment" 605 | "propagate_at_launch" = "true" 606 | "value" = "Public" 607 | }, 608 | { 609 | "additional_tag" = "yes" 610 | "key" = "Name" 611 | "propagate_at_launch" = "true" 612 | "value" = "charlie+uat+test+fire+water+earth+air" 613 | }, 614 | { 615 | "additional_tag" = "yes" 616 | "key" = "Namespace" 617 | "propagate_at_launch" = "true" 618 | "value" = "cloudposse" 619 | }, 620 | { 621 | "additional_tag" = "yes" 622 | "key" = "Stage" 623 | "propagate_at_launch" = "true" 624 | "value" = "test" 625 | }, 626 | ] 627 | label3 = { 628 | "attributes" = tolist([ 629 | "fire", 630 | "water", 631 | "earth", 632 | "air", 633 | ]) 634 | "delimiter" = "." 635 | "id" = "starfish.h.r.h.uat.release.fire.water.earth.air" 636 | "name" = "starfish" 637 | "namespace" = "cloudposse" 638 | "stage" = "release" 639 | "tenant" = "h.r.h" 640 | } 641 | label3_context = { 642 | "additional_tag_map" = {} 643 | "attributes" = tolist([ 644 | "fire", 645 | "water", 646 | "earth", 647 | "air", 648 | ]) 649 | "delimiter" = "." 650 | "enabled" = true 651 | "environment" = "UAT" 652 | "id_length_limit" = tonumber(null) 653 | "label_key_case" = tostring(null) 654 | "label_order" = tolist([ 655 | "name", 656 | "tenant", 657 | "environment", 658 | "stage", 659 | "attributes", 660 | ]) 661 | "label_value_case" = tostring(null) 662 | "name" = "Starfish" 663 | "namespace" = "CloudPosse" 664 | "regex_replace_chars" = "/[^-a-zA-Z0-9.]/" 665 | "stage" = "release" 666 | "tags" = { 667 | "Animal" = "Rabbit" 668 | "City" = "Dublin" 669 | "Eat" = "Carrot" 670 | "Environment" = "Private" 671 | } 672 | "tenant" = "H.R.H" 673 | } 674 | label3_normalized_context = { 675 | "additional_tag_map" = {} 676 | "attributes" = tolist([ 677 | "fire", 678 | "water", 679 | "earth", 680 | "air", 681 | ]) 682 | "delimiter" = "." 683 | "enabled" = true 684 | "environment" = "uat" 685 | "id_length_limit" = 0 686 | "label_key_case" = "title" 687 | "label_order" = tolist([ 688 | "name", 689 | "tenant", 690 | "environment", 691 | "stage", 692 | "attributes", 693 | ]) 694 | "label_value_case" = "lower" 695 | "name" = "starfish" 696 | "namespace" = "cloudposse" 697 | "regex_replace_chars" = "/[^-a-zA-Z0-9.]/" 698 | "stage" = "release" 699 | "tags" = { 700 | "Animal" = "Rabbit" 701 | "Attributes" = "fire.water.earth.air" 702 | "City" = "Dublin" 703 | "Eat" = "Carrot" 704 | "Environment" = "Private" 705 | "Name" = "starfish.h.r.h.uat.release.fire.water.earth.air" 706 | "Namespace" = "cloudposse" 707 | "Stage" = "release" 708 | "Tenant" = "h.r.h" 709 | } 710 | "tenant" = "h.r.h" 711 | } 712 | label3_tags = tomap({ 713 | "Animal" = "Rabbit" 714 | "Attributes" = "fire.water.earth.air" 715 | "City" = "Dublin" 716 | "Eat" = "Carrot" 717 | "Environment" = "Private" 718 | "Name" = "starfish.h.r.h.uat.release.fire.water.earth.air" 719 | "Namespace" = "cloudposse" 720 | "Stage" = "release" 721 | "Tenant" = "h.r.h" 722 | }) 723 | 724 | ``` 725 | 726 |
727 | 728 | > [!IMPORTANT] 729 | > In Cloud Posse's examples, we avoid pinning modules to specific versions to prevent discrepancies between the documentation 730 | > and the latest released versions. However, for your own projects, we strongly advise pinning each module to the exact version 731 | > you're using. This practice ensures the stability of your infrastructure. Additionally, we recommend implementing a systematic 732 | > approach for updating versions to avoid unexpected changes. 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | ## Requirements 743 | 744 | | Name | Version | 745 | |------|---------| 746 | | [terraform](#requirement\_terraform) | >= 0.13.0 | 747 | 748 | ## Providers 749 | 750 | No providers. 751 | 752 | ## Modules 753 | 754 | No modules. 755 | 756 | ## Resources 757 | 758 | No resources. 759 | 760 | ## Inputs 761 | 762 | | Name | Description | Type | Default | Required | 763 | |------|-------------|------|---------|:--------:| 764 | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | 765 | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | 766 | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | 767 | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | 768 | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | 769 | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | 770 | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | 771 | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | 772 | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | 773 | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | 774 | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | 775 | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | 776 | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | 777 | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | 778 | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | 779 | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | 780 | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | 781 | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | 782 | 783 | ## Outputs 784 | 785 | | Name | Description | 786 | |------|-------------| 787 | | [additional\_tag\_map](#output\_additional\_tag\_map) | The merged additional\_tag\_map | 788 | | [attributes](#output\_attributes) | List of attributes | 789 | | [context](#output\_context) | Merged but otherwise unmodified input to this module, to be used as context input to other modules.
Note: this version will have null values as defaults, not the values actually used as defaults. | 790 | | [delimiter](#output\_delimiter) | Delimiter between `namespace`, `tenant`, `environment`, `stage`, `name` and `attributes` | 791 | | [descriptors](#output\_descriptors) | Map of descriptors as configured by `descriptor_formats` | 792 | | [enabled](#output\_enabled) | True if module is enabled, false otherwise | 793 | | [environment](#output\_environment) | Normalized environment | 794 | | [id](#output\_id) | Disambiguated ID string restricted to `id_length_limit` characters in total | 795 | | [id\_full](#output\_id\_full) | ID string not restricted in length | 796 | | [id\_length\_limit](#output\_id\_length\_limit) | The id\_length\_limit actually used to create the ID, with `0` meaning unlimited | 797 | | [label\_order](#output\_label\_order) | The naming order actually used to create the ID | 798 | | [name](#output\_name) | Normalized name | 799 | | [namespace](#output\_namespace) | Normalized namespace | 800 | | [normalized\_context](#output\_normalized\_context) | Normalized context of this module | 801 | | [regex\_replace\_chars](#output\_regex\_replace\_chars) | The regex\_replace\_chars actually used to create the ID | 802 | | [stage](#output\_stage) | Normalized stage | 803 | | [tags](#output\_tags) | Normalized Tag map | 804 | | [tags\_as\_list\_of\_maps](#output\_tags\_as\_list\_of\_maps) | This is a list with one map for each `tag`. Each map contains the tag `key`,
`value`, and contents of `var.additional_tag_map`. Used in the rare cases
where resources need additional configuration information for each tag. | 805 | | [tenant](#output\_tenant) | Normalized tenant | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | > [!TIP] 817 | > #### Use Terraform Reference Architectures for AWS 818 | > 819 | > Use Cloud Posse's ready-to-go [terraform architecture blueprints](https://cloudposse.com/reference-architecture/) for AWS to get up and running quickly. 820 | > 821 | > ✅ We build it together with your team.
822 | > ✅ Your team owns everything.
823 | > ✅ 100% Open Source and backed by fanatical support.
824 | > 825 | > Request Quote 826 | >
📚 Learn More 827 | > 828 | >
829 | > 830 | > Cloud Posse is the leading [**DevOps Accelerator**](https://cpco.io/commercial-support?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-null-label&utm_content=commercial_support) for funded startups and enterprises. 831 | > 832 | > *Your team can operate like a pro today.* 833 | > 834 | > Ensure that your team succeeds by using Cloud Posse's proven process and turnkey blueprints. Plus, we stick around until you succeed. 835 | > #### Day-0: Your Foundation for Success 836 | > - **Reference Architecture.** You'll get everything you need from the ground up built using 100% infrastructure as code. 837 | > - **Deployment Strategy.** Adopt a proven deployment strategy with GitHub Actions, enabling automated, repeatable, and reliable software releases. 838 | > - **Site Reliability Engineering.** Gain total visibility into your applications and services with Datadog, ensuring high availability and performance. 839 | > - **Security Baseline.** Establish a secure environment from the start, with built-in governance, accountability, and comprehensive audit logs, safeguarding your operations. 840 | > - **GitOps.** Empower your team to manage infrastructure changes confidently and efficiently through Pull Requests, leveraging the full power of GitHub Actions. 841 | > 842 | > Request Quote 843 | > 844 | > #### Day-2: Your Operational Mastery 845 | > - **Training.** Equip your team with the knowledge and skills to confidently manage the infrastructure, ensuring long-term success and self-sufficiency. 846 | > - **Support.** Benefit from a seamless communication over Slack with our experts, ensuring you have the support you need, whenever you need it. 847 | > - **Troubleshooting.** Access expert assistance to quickly resolve any operational challenges, minimizing downtime and maintaining business continuity. 848 | > - **Code Reviews.** Enhance your team’s code quality with our expert feedback, fostering continuous improvement and collaboration. 849 | > - **Bug Fixes.** Rely on our team to troubleshoot and resolve any issues, ensuring your systems run smoothly. 850 | > - **Migration Assistance.** Accelerate your migration process with our dedicated support, minimizing disruption and speeding up time-to-value. 851 | > - **Customer Workshops.** Engage with our team in weekly workshops, gaining insights and strategies to continuously improve and innovate. 852 | > 853 | > Request Quote 854 | >
855 | 856 | ## ✨ Contributing 857 | 858 | This project is under active development, and we encourage contributions from our community. 859 | 860 | 861 | 862 | Many thanks to our outstanding contributors: 863 | 864 | 865 | 866 | 867 | 868 | For 🐛 bug reports & feature requests, please use the [issue tracker](https://github.com/cloudposse/terraform-null-label/issues). 869 | 870 | In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. 871 | 1. Review our [Code of Conduct](https://github.com/cloudposse/terraform-null-label/?tab=coc-ov-file#code-of-conduct) and [Contributor Guidelines](https://github.com/cloudposse/.github/blob/main/CONTRIBUTING.md). 872 | 2. **Fork** the repo on GitHub 873 | 3. **Clone** the project to your own machine 874 | 4. **Commit** changes to your own branch 875 | 5. **Push** your work back up to your fork 876 | 6. Submit a **Pull Request** so that we can review your changes 877 | 878 | **NOTE:** Be sure to merge the latest changes from "upstream" before making a pull request!## Running Terraform Tests 879 | 880 | We use [Atmos](https://atmos.tools) to streamline how Terraform tests are run. It centralizes configuration and wraps common test workflows with easy-to-use commands. 881 | 882 | All tests are located in the [`test/`](test) folder. 883 | 884 | Under the hood, tests are powered by Terratest together with our internal [Test Helpers](https://github.com/cloudposse/test-helpers) library, providing robust infrastructure validation. 885 | 886 | Setup dependencies: 887 | - Install Atmos ([installation guide](https://atmos.tools/install/)) 888 | - Install Go [1.24+ or newer](https://go.dev/doc/install) 889 | - Install Terraform or OpenTofu 890 | 891 | To run tests: 892 | 893 | - Run all tests: 894 | ```sh 895 | atmos test run 896 | ``` 897 | - Clean up test artifacts: 898 | ```sh 899 | atmos test clean 900 | ``` 901 | - Explore additional test options: 902 | ```sh 903 | atmos test --help 904 | ``` 905 | The configuration for test commands is centrally managed. To review what's being imported, see the [`atmos.yaml`](https://raw.githubusercontent.com/cloudposse/.github/refs/heads/main/.github/atmos/terraform-module.yaml) file. 906 | 907 | Learn more about our [automated testing in our documentation](https://docs.cloudposse.com/community/contribute/automated-testing/) or implementing [custom commands](https://atmos.tools/core-concepts/custom-commands/) with atmos. 908 | 909 | ### 🌎 Slack Community 910 | 911 | Join our [Open Source Community](https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-null-label&utm_content=slack) on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure. 912 | 913 | ### 📰 Newsletter 914 | 915 | Sign up for [our newsletter](https://cpco.io/newsletter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-null-label&utm_content=newsletter) and join 3,000+ DevOps engineers, CTOs, and founders who get insider access to the latest DevOps trends, so you can always stay in the know. 916 | Dropped straight into your Inbox every week — and usually a 5-minute read. 917 | 918 | ### 📆 Office Hours 919 | 920 | [Join us every Wednesday via Zoom](https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-null-label&utm_content=office_hours) for your weekly dose of insider DevOps trends, AWS news and Terraform insights, all sourced from our SweetOps community, plus a _live Q&A_ that you can’t find anywhere else. 921 | It's **FREE** for everyone! 922 | ## License 923 | 924 | License 925 | 926 |
927 | Preamble to the Apache License, Version 2.0 928 |
929 |
930 | 931 | Complete license is available in the [`LICENSE`](LICENSE) file. 932 | 933 | ```text 934 | Licensed to the Apache Software Foundation (ASF) under one 935 | or more contributor license agreements. See the NOTICE file 936 | distributed with this work for additional information 937 | regarding copyright ownership. The ASF licenses this file 938 | to you under the Apache License, Version 2.0 (the 939 | "License"); you may not use this file except in compliance 940 | with the License. You may obtain a copy of the License at 941 | 942 | https://www.apache.org/licenses/LICENSE-2.0 943 | 944 | Unless required by applicable law or agreed to in writing, 945 | software distributed under the License is distributed on an 946 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 947 | KIND, either express or implied. See the License for the 948 | specific language governing permissions and limitations 949 | under the License. 950 | ``` 951 |
952 | 953 | ## Trademarks 954 | 955 | All other trademarks referenced herein are the property of their respective owners. 956 | 957 | 958 | --- 959 | Copyright © 2017-2025 [Cloud Posse, LLC](https://cpco.io/copyright) 960 | 961 | 962 | README footer 963 | 964 | Beacon 965 | -------------------------------------------------------------------------------- /README.yaml: -------------------------------------------------------------------------------- 1 | name: terraform-null-label 2 | tags: 3 | - aws 4 | - terraform 5 | - terraform-modules 6 | - naming-convention 7 | - name 8 | - namespace 9 | - stage 10 | categories: 11 | - terraform-modules/supported 12 | license: APACHE2 13 | github_repo: cloudposse/terraform-null-label 14 | badges: 15 | - name: Latest Release 16 | image: https://img.shields.io/github/release/cloudposse/terraform-null-label.svg?style=for-the-badge 17 | url: https://github.com/cloudposse/terraform-null-label/releases/latest 18 | - name: Last Updated 19 | image: https://img.shields.io/github/last-commit/cloudposse/terraform-null-label.svg?style=for-the-badge 20 | url: https://github.com/cloudposse/terraform-null-label/commits 21 | - name: Slack Community 22 | image: https://slack.cloudposse.com/for-the-badge.svg 23 | url: https://cloudposse.com/slack 24 | description: |- 25 | Terraform module designed to generate consistent names and tags for resources. Use `terraform-null-label` to implement a strict naming convention. 26 | 27 | There are 6 inputs considered "labels" or "ID elements" (because the labels are used to construct the ID): 28 | 1. namespace 29 | 1. tenant 30 | 1. environment 31 | 1. stage 32 | 1. name 33 | 1. attributes 34 | 35 | This module generates IDs using the following convention by default: `{namespace}-{environment}-{stage}-{name}-{attributes}`. 36 | However, it is highly configurable. The delimiter (e.g. `-`) is configurable. Each label item is optional (although you must provide at least one). 37 | So if you prefer the term `stage` to `environment` and do not need `tenant`, you can exclude them 38 | and the label `id` will look like `{namespace}-{stage}-{name}-{attributes}`. 39 | - The `tenant` label was introduced in v0.25.0. To preserve backward compatibility, it is not included by default. 40 | - The `attributes` input is actually a list of strings and `{attributes}` expands to the list elements joined by the delimiter. 41 | - If `attributes` is excluded but `namespace`, `stage`, and `environment` are included, `id` will look like `{namespace}-{environment}-{stage}-{name}`. 42 | Excluding `attributes` is discouraged, though, because attributes are the main way modules modify the ID to ensure uniqueness when provisioning the same resource types. 43 | - If you want the label items in a different order, you can specify that, too, with the `label_order` list. 44 | - You can set a maximum length for the `id`, and the module will create a (probably) unique name that fits within that length. 45 | (The module uses a portion of the MD5 hash of the full `id` to represent the missing part, so there remains a slight chance of name collision.) 46 | - You can control the letter case of the generated labels which make up the `id` using `var.label_value_case`. 47 | - By default, all of the non-empty labels are also exported as tags, whether they appear in the `id` or not. 48 | You can control which labels are exported as tags by setting `labels_as_tags` to the list of labels you want exported, 49 | or the empty list `[]` if you want no labels exported as tags at all. Tags passed in via the `tags` variable are 50 | always exported, and regardless of settings, empty labels are never exported as tags. 51 | You can control the case of the tag names (keys) for the labels using `var.label_key_case`. 52 | Unlike the tags generated from the label inputs, tags passed in via the `tags` input are not modified. 53 | 54 | There is an unfortunate collision over the use of the key `name`. Cloud Posse uses `name` in this module 55 | to represent the component, such as `eks` or `rds`. AWS uses a tag with the key `Name` to store the full human-friendly 56 | identifier of the thing tagged, which this module outputs as `id`, not `name`. So when converting input labels 57 | to tags, the value of the `Name` key is set to the module `id` output, and there is no tag corresponding to the 58 | module `name` output. An empty `name` label will not prevent the `Name` tag from being exported. 59 | 60 | It's recommended to use one `terraform-null-label` module for every unique resource of a given resource type. 61 | For example, if you have 10 instances, there should be 10 different labels. 62 | However, if you have multiple different kinds of resources (e.g. instances, security groups, file systems, and elastic ips), then they can all share the same label assuming they are logically related. 63 | 64 | For most purposes, the `id` output is sufficient to create an ID or label for a resource, and if you want a different 65 | ID or a different format, you would instantiate another instance of `null-label` and configure it accordingly. However, 66 | to accomodate situations where you want all the same inputs to generate multiple descriptors, this module provides 67 | the `descriptors` output, which is a map of strings generated according to the format specified by the 68 | `descriptor_formats` input. This feature is intentionally simple and minimally configurable and will not be 69 | enhanced to add more features that are already in `null-label`. See [examples/complete/descriptors.tf](examples/complete/descriptors.tf) for examples. 70 | 71 | All [Cloud Posse Terraform modules](https://github.com/cloudposse?utf8=%E2%9C%93&q=terraform-&type=&language=) use this module to ensure resources can be instantiated multiple times within an account and without conflict. 72 | 73 | The Cloud Posse convention is to use labels as follows: 74 | - `namespace`: A short (3-4 letters) abbreviation of the company name, to ensure globally unique IDs for things like S3 buckets 75 | - `tenant`: _(Rarely needed)_ When a company creates a dedicated resource per customer, `tenant` can be used to identify the customer the resource is dedicated to 76 | - `environment`: A [short abbreviation](https://github.com/cloudposse/terraform-aws-utils/#introduction) for the AWS region hosting the resource, or `gbl` for resources like IAM roles that have no region 77 | - `stage`: The name or role of the account the resource is for, such as `prod` or `dev` 78 | - `name`: The name of the component that owns the resources, such as `eks` or `rds` 79 | 80 | **NOTE:** The `null` originally referred to the primary Terraform [provider](https://www.terraform.io/docs/providers/null/index.html) used in this module. 81 | With Terraform 0.12, this module no longer needs any provider, but the name was kept for continuity. 82 | 83 | - Releases of this module from `0.23.0` onward only work with Terraform 0.13 or newer. 84 | - Releases of this module from `0.12.0` through `0.22.1` support `HCL2` and are compatible with Terraform 0.12 or newer. 85 | - Releases of this module prior to `0.12.0` are compatible with earlier versions of terraform like Terraform 0.11. 86 | usage: |- 87 | ### Defaults 88 | 89 | Cloud Posse Terraform modules share a common `context` object that is meant to be passed from module to module. 90 | The context object is a single object that contains all the input values for `terraform-null-label`. 91 | However, each input value can also be specified individually by name as a standard Terraform variable, 92 | and the value of those variables, when set to something other than `null`, will override the value 93 | in the context object. In order to allow chaining of these objects, where the context object input to one 94 | module is transformed and passed on to the next module, all the variables default to `null` or empty collections. 95 | The actual default values used when nothing is explicitly set are described in the documentation below. 96 | 97 | For example, the default value of `delimiter` is shown as `null`, but if you leave it set to `null`, 98 | `terraform-null-label` will actually use the default delimiter `-` (hyphen). 99 | 100 | A non-obvious but intentional consequence of this design is that once a module sets a non-default value, 101 | future modules in the chain cannot reset the value back to the original default. Instead, the new setting 102 | becomes the new default for downstream modules. Also, collections are not overwritten, they are merged, 103 | so once a tag is added, it will remain in the tag set and cannot be removed, although its value can 104 | be overwritten. 105 | 106 | Because the purpose of `labels_as_tags` is primarily to prevent tags from being generated 107 | that would [conflict with the AWS provider's `default_tags`](https://github.com/hashicorp/terraform-provider-aws/issues/19204), it is an exception to the 108 | rule that variables override the setting in the context object. The value in the context 109 | object cannot be changed, so that later modules cannot re-enable a problematic tag. 110 | 111 | ### Simple Example 112 | 113 | ```hcl 114 | module "eg_prod_bastion_label" { 115 | source = "cloudposse/label/null" 116 | # Cloud Posse recommends pinning every module to a specific version 117 | # version = "x.x.x" 118 | 119 | namespace = "eg" 120 | stage = "prod" 121 | name = "bastion" 122 | attributes = ["public"] 123 | delimiter = "-" 124 | 125 | tags = { 126 | "BusinessUnit" = "XYZ", 127 | "Snapshot" = "true" 128 | } 129 | } 130 | ``` 131 | 132 | This will create an `id` with the value of `eg-prod-bastion-public` because when generating `id`, the default order is `namespace`, `environment`, `stage`, `name`, `attributes` 133 | (you can override it by using the `label_order` variable, see [Advanced Example 3](#advanced-example-3)). 134 | 135 | Now reference the label when creating an instance: 136 | 137 | ```hcl 138 | resource "aws_instance" "eg_prod_bastion_public" { 139 | instance_type = "t1.micro" 140 | tags = module.eg_prod_bastion_label.tags 141 | } 142 | ``` 143 | 144 | Or define a security group: 145 | 146 | ```hcl 147 | resource "aws_security_group" "eg_prod_bastion_public" { 148 | vpc_id = var.vpc_id 149 | name = module.eg_prod_bastion_label.id 150 | tags = module.eg_prod_bastion_label.tags 151 | egress { 152 | from_port = 0 153 | to_port = 0 154 | protocol = "-1" 155 | cidr_blocks = ["0.0.0.0/0"] 156 | } 157 | } 158 | ``` 159 | 160 | 161 | ### Advanced Example 162 | 163 | Here is a more complex example with two instances using two different labels. Note how efficiently the tags are defined for both the instance and the security group. 164 | 165 |
Click to show 166 | 167 | ```hcl 168 | module "eg_prod_bastion_label" { 169 | source = "cloudposse/label/null" 170 | # Cloud Posse recommends pinning every module to a specific version 171 | # version = "x.x.x" 172 | 173 | namespace = "eg" 174 | stage = "prod" 175 | name = "bastion" 176 | delimiter = "-" 177 | 178 | tags = { 179 | "BusinessUnit" = "XYZ", 180 | "Snapshot" = "true" 181 | } 182 | } 183 | 184 | module "eg_prod_bastion_abc_label" { 185 | source = "cloudposse/label/null" 186 | # Cloud Posse recommends pinning every module to a specific version 187 | # version = "x.x.x" 188 | 189 | attributes = ["abc"] 190 | 191 | tags = { 192 | "BusinessUnit" = "ABC" # Override the Business Unit tag set in the base label 193 | } 194 | 195 | # Copy all other fields from the base label 196 | context = module.eg_prod_bastion_label.context 197 | } 198 | 199 | resource "aws_security_group" "eg_prod_bastion_abc" { 200 | name = module.eg_prod_bastion_abc_label.id 201 | tags = module.eg_prod_bastion_abc_label.tags 202 | ingress { 203 | from_port = 22 204 | to_port = 22 205 | protocol = "tcp" 206 | cidr_blocks = ["0.0.0.0/0"] 207 | } 208 | } 209 | 210 | resource "aws_instance" "eg_prod_bastion_abc" { 211 | instance_type = "t1.micro" 212 | tags = module.eg_prod_bastion_abc_label.tags 213 | vpc_security_group_ids = [aws_security_group.eg_prod_bastion_abc.id] 214 | } 215 | 216 | module "eg_prod_bastion_xyz_label" { 217 | source = "cloudposse/label/null" 218 | # Cloud Posse recommends pinning every module to a specific version 219 | # version = "x.x.x" 220 | 221 | attributes = ["xyz"] 222 | 223 | context = module.eg_prod_bastion_label.context 224 | } 225 | 226 | resource "aws_security_group" "eg_prod_bastion_xyz" { 227 | name = module.eg_prod_bastion_xyz_label.id 228 | tags = module.eg_prod_bastion_xyz_label.tags 229 | ingress { 230 | from_port = 22 231 | to_port = 22 232 | protocol = "tcp" 233 | cidr_blocks = ["0.0.0.0/0"] 234 | } 235 | } 236 | 237 | resource "aws_instance" "eg_prod_bastion_xyz" { 238 | instance_type = "t1.micro" 239 | tags = module.eg_prod_bastion_xyz_label.tags 240 | vpc_security_group_ids = [aws_security_group.eg_prod_bastion_xyz.id] 241 | } 242 | ``` 243 | 244 |
245 | 246 | ### Advanced Example 2 247 | 248 | Here is a more complex example with an autoscaling group that has a different tagging schema than other resources and 249 | requires its tags to be in this format, which this module can generate via `additional_tag_map` and `tags_as_list_of_maps`: 250 | 251 |
Click to show 252 | 253 | ```hcl 254 | tags = [ 255 | { 256 | key = "Name", 257 | propagate_at_launch = true, 258 | value = "namespace-stage-name" 259 | }, 260 | { 261 | key = "Namespace", 262 | propagate_at_launch = true, 263 | value = "namespace" 264 | }, 265 | { 266 | key = "Stage", 267 | propagate_at_launch = true, 268 | value = "stage" 269 | } 270 | ] 271 | ``` 272 | 273 | Autoscaling group using propagating tagging below (full example: [autoscalinggroup](examples/autoscalinggroup/main.tf)) 274 | 275 | ```hcl 276 | ################################ 277 | # terraform-null-label example # 278 | ################################ 279 | module "label" { 280 | source = "../../" 281 | namespace = "cp" 282 | stage = "prod" 283 | name = "app" 284 | 285 | tags = { 286 | BusinessUnit = "Finance" 287 | ManagedBy = "Terraform" 288 | } 289 | 290 | additional_tag_map = { 291 | propagate_at_launch = true 292 | } 293 | } 294 | 295 | ####################### 296 | # Launch template # 297 | ####################### 298 | resource "aws_launch_template" "default" { 299 | # terraform-null-label example used here: Set template name prefix 300 | name_prefix = "${module.label.id}-" 301 | image_id = data.aws_ami.amazon_linux.id 302 | instance_type = "t2.micro" 303 | instance_initiated_shutdown_behavior = "terminate" 304 | 305 | vpc_security_group_ids = [data.aws_security_group.default.id] 306 | 307 | monitoring { 308 | enabled = false 309 | } 310 | # terraform-null-label example used here: Set tags on volumes 311 | tag_specifications { 312 | resource_type = "volume" 313 | tags = module.label.tags 314 | } 315 | } 316 | 317 | ###################### 318 | # Autoscaling group # 319 | ###################### 320 | resource "aws_autoscaling_group" "default" { 321 | # terraform-null-label example used here: Set ASG name prefix 322 | name_prefix = "${module.label.id}-" 323 | vpc_zone_identifier = data.aws_subnet_ids.all.ids 324 | max_size = 1 325 | min_size = 1 326 | desired_capacity = 1 327 | 328 | launch_template = { 329 | id = aws_launch_template.default.id 330 | version = "$$Latest" 331 | } 332 | 333 | # terraform-null-label example used here: Set tags on ASG and EC2 Servers 334 | tags = module.label.tags_as_list_of_maps 335 | } 336 | ``` 337 | 338 |
339 | 340 | ### Advanced Example 3 341 | 342 | See [complete example](./examples/complete) for even more examples. 343 | 344 | This example shows how you can pass the `context` output of one label module to the next label_module, 345 | allowing you to create one label that has the base set of values, and then creating every extra label 346 | as a derivative of that. 347 | 348 |
Click to show 349 | 350 | ```hcl 351 | module "label1" { 352 | source = "cloudposse/label/null" 353 | # Cloud Posse recommends pinning every module to a specific version 354 | # version = "x.x.x" 355 | 356 | namespace = "CloudPosse" 357 | tenant = "H.R.H" 358 | environment = "UAT" 359 | stage = "build" 360 | name = "Winston Churchroom" 361 | attributes = ["fire", "water", "earth", "air"] 362 | 363 | label_order = ["name", "tenant", "environment", "stage", "attributes"] 364 | 365 | tags = { 366 | "City" = "Dublin" 367 | "Environment" = "Private" 368 | } 369 | } 370 | 371 | module "label2" { 372 | source = "cloudposse/label/null" 373 | # Cloud Posse recommends pinning every module to a specific version 374 | # version = "x.x.x" 375 | 376 | name = "Charlie" 377 | tenant = "" # setting to `null` would have no effect 378 | stage = "test" 379 | delimiter = "+" 380 | regex_replace_chars = "/[^a-zA-Z0-9-+]/" 381 | 382 | additional_tag_map = { 383 | propagate_at_launch = true 384 | additional_tag = "yes" 385 | } 386 | 387 | tags = { 388 | "City" = "London" 389 | "Environment" = "Public" 390 | } 391 | 392 | context = module.label1.context 393 | } 394 | 395 | module "label3" { 396 | source = "cloudposse/label/null" 397 | # Cloud Posse recommends pinning every module to a specific version 398 | # version = "x.x.x" 399 | 400 | name = "Starfish" 401 | stage = "release" 402 | delimiter = "." 403 | regex_replace_chars = "/[^-a-zA-Z0-9.]/" 404 | 405 | tags = { 406 | "Eat" = "Carrot" 407 | "Animal" = "Rabbit" 408 | } 409 | 410 | context = module.label1.context 411 | } 412 | ``` 413 | 414 | This creates label outputs like this: 415 | 416 | ```hcl 417 | label1 = { 418 | "attributes" = tolist([ 419 | "fire", 420 | "water", 421 | "earth", 422 | "air", 423 | ]) 424 | "delimiter" = "-" 425 | "id" = "winstonchurchroom-hrh-uat-build-fire-water-earth-air" 426 | "name" = "winstonchurchroom" 427 | "namespace" = "cloudposse" 428 | "stage" = "build" 429 | "tenant" = "hrh" 430 | } 431 | label1_context = { 432 | "additional_tag_map" = {} 433 | "attributes" = tolist([ 434 | "fire", 435 | "water", 436 | "earth", 437 | "air", 438 | ]) 439 | "delimiter" = tostring(null) 440 | "enabled" = true 441 | "environment" = "UAT" 442 | "id_length_limit" = tonumber(null) 443 | "label_key_case" = tostring(null) 444 | "label_order" = tolist([ 445 | "name", 446 | "tenant", 447 | "environment", 448 | "stage", 449 | "attributes", 450 | ]) 451 | "label_value_case" = tostring(null) 452 | "name" = "Winston Churchroom" 453 | "namespace" = "CloudPosse" 454 | "regex_replace_chars" = tostring(null) 455 | "stage" = "build" 456 | "tags" = { 457 | "City" = "Dublin" 458 | "Environment" = "Private" 459 | } 460 | "tenant" = "H.R.H" 461 | } 462 | label1_normalized_context = { 463 | "additional_tag_map" = {} 464 | "attributes" = tolist([ 465 | "fire", 466 | "water", 467 | "earth", 468 | "air", 469 | ]) 470 | "delimiter" = "-" 471 | "enabled" = true 472 | "environment" = "uat" 473 | "id_length_limit" = 0 474 | "label_key_case" = "title" 475 | "label_order" = tolist([ 476 | "name", 477 | "tenant", 478 | "environment", 479 | "stage", 480 | "attributes", 481 | ]) 482 | "label_value_case" = "lower" 483 | "name" = "winstonchurchroom" 484 | "namespace" = "cloudposse" 485 | "regex_replace_chars" = "/[^-a-zA-Z0-9]/" 486 | "stage" = "build" 487 | "tags" = { 488 | "Attributes" = "fire-water-earth-air" 489 | "City" = "Dublin" 490 | "Environment" = "Private" 491 | "Name" = "winstonchurchroom-hrh-uat-build-fire-water-earth-air" 492 | "Namespace" = "cloudposse" 493 | "Stage" = "build" 494 | "Tenant" = "hrh" 495 | } 496 | "tenant" = "hrh" 497 | } 498 | label1_tags = tomap({ 499 | "Attributes" = "fire-water-earth-air" 500 | "City" = "Dublin" 501 | "Environment" = "Private" 502 | "Name" = "winstonchurchroom-hrh-uat-build-fire-water-earth-air" 503 | "Namespace" = "cloudposse" 504 | "Stage" = "build" 505 | "Tenant" = "hrh" 506 | }) 507 | label2 = { 508 | "attributes" = tolist([ 509 | "fire", 510 | "water", 511 | "earth", 512 | "air", 513 | ]) 514 | "delimiter" = "+" 515 | "id" = "charlie+uat+test+fire+water+earth+air" 516 | "name" = "charlie" 517 | "namespace" = "cloudposse" 518 | "stage" = "test" 519 | "tenant" = "" 520 | } 521 | label2_context = { 522 | "additional_tag_map" = { 523 | "additional_tag" = "yes" 524 | "propagate_at_launch" = "true" 525 | } 526 | "attributes" = tolist([ 527 | "fire", 528 | "water", 529 | "earth", 530 | "air", 531 | ]) 532 | "delimiter" = "+" 533 | "enabled" = true 534 | "environment" = "UAT" 535 | "id_length_limit" = tonumber(null) 536 | "label_key_case" = tostring(null) 537 | "label_order" = tolist([ 538 | "name", 539 | "tenant", 540 | "environment", 541 | "stage", 542 | "attributes", 543 | ]) 544 | "label_value_case" = tostring(null) 545 | "name" = "Charlie" 546 | "namespace" = "CloudPosse" 547 | "regex_replace_chars" = "/[^a-zA-Z0-9-+]/" 548 | "stage" = "test" 549 | "tags" = { 550 | "City" = "London" 551 | "Environment" = "Public" 552 | } 553 | "tenant" = "" 554 | } 555 | label2_tags = tomap({ 556 | "Attributes" = "fire+water+earth+air" 557 | "City" = "London" 558 | "Environment" = "Public" 559 | "Name" = "charlie+uat+test+fire+water+earth+air" 560 | "Namespace" = "cloudposse" 561 | "Stage" = "test" 562 | }) 563 | label2_tags_as_list_of_maps = [ 564 | { 565 | "additional_tag" = "yes" 566 | "key" = "Attributes" 567 | "propagate_at_launch" = "true" 568 | "value" = "fire+water+earth+air" 569 | }, 570 | { 571 | "additional_tag" = "yes" 572 | "key" = "City" 573 | "propagate_at_launch" = "true" 574 | "value" = "London" 575 | }, 576 | { 577 | "additional_tag" = "yes" 578 | "key" = "Environment" 579 | "propagate_at_launch" = "true" 580 | "value" = "Public" 581 | }, 582 | { 583 | "additional_tag" = "yes" 584 | "key" = "Name" 585 | "propagate_at_launch" = "true" 586 | "value" = "charlie+uat+test+fire+water+earth+air" 587 | }, 588 | { 589 | "additional_tag" = "yes" 590 | "key" = "Namespace" 591 | "propagate_at_launch" = "true" 592 | "value" = "cloudposse" 593 | }, 594 | { 595 | "additional_tag" = "yes" 596 | "key" = "Stage" 597 | "propagate_at_launch" = "true" 598 | "value" = "test" 599 | }, 600 | ] 601 | label3 = { 602 | "attributes" = tolist([ 603 | "fire", 604 | "water", 605 | "earth", 606 | "air", 607 | ]) 608 | "delimiter" = "." 609 | "id" = "starfish.h.r.h.uat.release.fire.water.earth.air" 610 | "name" = "starfish" 611 | "namespace" = "cloudposse" 612 | "stage" = "release" 613 | "tenant" = "h.r.h" 614 | } 615 | label3_context = { 616 | "additional_tag_map" = {} 617 | "attributes" = tolist([ 618 | "fire", 619 | "water", 620 | "earth", 621 | "air", 622 | ]) 623 | "delimiter" = "." 624 | "enabled" = true 625 | "environment" = "UAT" 626 | "id_length_limit" = tonumber(null) 627 | "label_key_case" = tostring(null) 628 | "label_order" = tolist([ 629 | "name", 630 | "tenant", 631 | "environment", 632 | "stage", 633 | "attributes", 634 | ]) 635 | "label_value_case" = tostring(null) 636 | "name" = "Starfish" 637 | "namespace" = "CloudPosse" 638 | "regex_replace_chars" = "/[^-a-zA-Z0-9.]/" 639 | "stage" = "release" 640 | "tags" = { 641 | "Animal" = "Rabbit" 642 | "City" = "Dublin" 643 | "Eat" = "Carrot" 644 | "Environment" = "Private" 645 | } 646 | "tenant" = "H.R.H" 647 | } 648 | label3_normalized_context = { 649 | "additional_tag_map" = {} 650 | "attributes" = tolist([ 651 | "fire", 652 | "water", 653 | "earth", 654 | "air", 655 | ]) 656 | "delimiter" = "." 657 | "enabled" = true 658 | "environment" = "uat" 659 | "id_length_limit" = 0 660 | "label_key_case" = "title" 661 | "label_order" = tolist([ 662 | "name", 663 | "tenant", 664 | "environment", 665 | "stage", 666 | "attributes", 667 | ]) 668 | "label_value_case" = "lower" 669 | "name" = "starfish" 670 | "namespace" = "cloudposse" 671 | "regex_replace_chars" = "/[^-a-zA-Z0-9.]/" 672 | "stage" = "release" 673 | "tags" = { 674 | "Animal" = "Rabbit" 675 | "Attributes" = "fire.water.earth.air" 676 | "City" = "Dublin" 677 | "Eat" = "Carrot" 678 | "Environment" = "Private" 679 | "Name" = "starfish.h.r.h.uat.release.fire.water.earth.air" 680 | "Namespace" = "cloudposse" 681 | "Stage" = "release" 682 | "Tenant" = "h.r.h" 683 | } 684 | "tenant" = "h.r.h" 685 | } 686 | label3_tags = tomap({ 687 | "Animal" = "Rabbit" 688 | "Attributes" = "fire.water.earth.air" 689 | "City" = "Dublin" 690 | "Eat" = "Carrot" 691 | "Environment" = "Private" 692 | "Name" = "starfish.h.r.h.uat.release.fire.water.earth.air" 693 | "Namespace" = "cloudposse" 694 | "Stage" = "release" 695 | "Tenant" = "h.r.h" 696 | }) 697 | 698 | ``` 699 | 700 |
701 | 702 | include: [] 703 | contributors: [] 704 | -------------------------------------------------------------------------------- /atmos.yaml: -------------------------------------------------------------------------------- 1 | # Atmos Configuration — powered by https://atmos.tools 2 | # 3 | # This configuration enables centralized, DRY, and consistent project scaffolding using Atmos. 4 | # 5 | # Included features: 6 | # - Organizational custom commands: https://atmos.tools/core-concepts/custom-commands 7 | # - Automated README generation: https://atmos.tools/cli/commands/docs/generate 8 | # 9 | 10 | # Import shared configuration used by all modules 11 | import: 12 | - https://raw.githubusercontent.com/cloudposse/.github/refs/heads/main/.github/atmos/terraform-module.yaml 13 | -------------------------------------------------------------------------------- /descriptors.tf: -------------------------------------------------------------------------------- 1 | # It would be nice to have a fixed array of arguments passed into 2 | # `format()` so all you need to provide is a format string, but 3 | # unfortunately, that does not work easily 4 | # due to https://github.com/hashicorp/terraform/issues/28558 5 | # which requires that the format string consume the last argument passed in. 6 | # We could hack around it by adding then removing a trailing arg, like 7 | # 8 | # trimsuffix(format("${var.format_string}%[${length(local.labels)+1}]v", concat(local.labels, ["x"])...), "x") 9 | # 10 | # but that is kind of a hack, and overlooks the fact that local.labels 11 | # drops empty label elements, so the index of an element is not guaranteed. 12 | # 13 | # 14 | # So we require the user to specify the arguments as well as the format string. 15 | # 16 | 17 | # There is a lot of room for enhancement, but since this is a new feature 18 | # with only 2 use cases, we are going to keep it simple for now. 19 | 20 | locals { 21 | descriptor_labels = { for k, v in local.descriptor_formats : k => [ 22 | for label in v.labels : local.id_context[label] 23 | ] } 24 | descriptors = { for k, v in local.descriptor_formats : k => ( 25 | format(v.format, local.descriptor_labels[k]...) 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/autoscalinggroup/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | **/.terraform/* 3 | *.tfstate 4 | *.tfstate.* 5 | *.tfvars 6 | !autoscalinggroup.auto.tfvars -------------------------------------------------------------------------------- /examples/autoscalinggroup/autoscalinggroup.auto.tfvars: -------------------------------------------------------------------------------- 1 | namespace = "eg" 2 | stage = "prod" 3 | name = "app" 4 | 5 | tags = { 6 | BusinessUnit = "Finance" 7 | ManagedBy = "Terraform" 8 | } 9 | 10 | additional_tag_map = { 11 | propagate_at_launch = "true" 12 | } 13 | -------------------------------------------------------------------------------- /examples/autoscalinggroup/context.tf: -------------------------------------------------------------------------------- 1 | # DO NOT COPY THIS FILE 2 | # 3 | # This is a specially modified version of this file, since it is used to test 4 | # the unpublished version of this module. Normally you should use a 5 | # copy of the file as explained below. 6 | # 7 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label 8 | # All other instances of this file should be a copy of that one 9 | # 10 | # 11 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf 12 | # and then place it in your Terraform module to automatically get 13 | # Cloud Posse's standard configuration inputs suitable for passing 14 | # to Cloud Posse modules. 15 | # 16 | # Modules should access the whole context as `module.this.context` 17 | # to get the input variables with nulls for defaults, 18 | # for example `context = module.this.context`, 19 | # and access individual variables as `module.this.`, 20 | # with final values filled in. 21 | # 22 | # For example, when using defaults, `module.this.context.delimiter` 23 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 24 | # 25 | 26 | module "this" { 27 | source = "../.." 28 | 29 | enabled = var.enabled 30 | namespace = var.namespace 31 | environment = var.environment 32 | stage = var.stage 33 | name = var.name 34 | delimiter = var.delimiter 35 | attributes = var.attributes 36 | tags = var.tags 37 | additional_tag_map = var.additional_tag_map 38 | label_order = var.label_order 39 | regex_replace_chars = var.regex_replace_chars 40 | id_length_limit = var.id_length_limit 41 | 42 | context = var.context 43 | } 44 | 45 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 46 | 47 | variable "context" { 48 | type = object({ 49 | enabled = bool 50 | namespace = string 51 | environment = string 52 | stage = string 53 | name = string 54 | delimiter = string 55 | attributes = list(string) 56 | tags = map(string) 57 | additional_tag_map = map(string) 58 | regex_replace_chars = string 59 | label_order = list(string) 60 | id_length_limit = number 61 | label_key_case = string 62 | label_value_case = string 63 | }) 64 | default = { 65 | enabled = true 66 | namespace = null 67 | environment = null 68 | stage = null 69 | name = null 70 | delimiter = null 71 | attributes = [] 72 | tags = {} 73 | additional_tag_map = {} 74 | regex_replace_chars = null 75 | label_order = [] 76 | id_length_limit = null 77 | label_key_case = null 78 | label_value_case = null 79 | } 80 | description = <<-EOT 81 | Single object for setting entire context at once. 82 | See description of individual variables for details. 83 | Leave string and numeric variables as `null` to use default value. 84 | Individual variable settings (non-null) override settings in context object, 85 | except for attributes, tags, and additional_tag_map, which are merged. 86 | EOT 87 | 88 | validation { 89 | condition = var.context["label_key_case"] == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 90 | error_message = "Allowed values: `lower`, `title`, `upper`." 91 | } 92 | 93 | validation { 94 | condition = var.context["label_value_case"] == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 95 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 96 | } 97 | } 98 | 99 | variable "enabled" { 100 | type = bool 101 | default = null 102 | description = "Set to false to prevent the module from creating any resources" 103 | } 104 | 105 | variable "namespace" { 106 | type = string 107 | default = null 108 | description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" 109 | } 110 | 111 | variable "environment" { 112 | type = string 113 | default = null 114 | description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" 115 | } 116 | 117 | variable "stage" { 118 | type = string 119 | default = null 120 | description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" 121 | } 122 | 123 | variable "name" { 124 | type = string 125 | default = null 126 | description = "Solution name, e.g. 'app' or 'jenkins'" 127 | } 128 | 129 | variable "delimiter" { 130 | type = string 131 | default = null 132 | description = <<-EOT 133 | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. 134 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 135 | EOT 136 | } 137 | 138 | variable "attributes" { 139 | type = list(string) 140 | default = [] 141 | description = "Additional attributes (e.g. `1`)" 142 | } 143 | 144 | variable "tags" { 145 | type = map(string) 146 | default = {} 147 | description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" 148 | } 149 | 150 | variable "additional_tag_map" { 151 | type = map(string) 152 | default = {} 153 | description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." 154 | } 155 | 156 | variable "label_order" { 157 | type = list(string) 158 | default = null 159 | description = <<-EOT 160 | The naming order of the id output and Name tag. 161 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 162 | You can omit any of the 5 elements, but at least one must be present. 163 | EOT 164 | } 165 | 166 | variable "regex_replace_chars" { 167 | type = string 168 | default = null 169 | description = <<-EOT 170 | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. 171 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 172 | EOT 173 | } 174 | 175 | variable "id_length_limit" { 176 | type = number 177 | default = null 178 | description = <<-EOT 179 | Limit `id` to this many characters. 180 | Set to `0` for unlimited length. 181 | Set to `null` for default, which is `0`. 182 | Does not affect `id_full`. 183 | EOT 184 | } 185 | 186 | variable "label_key_case" { 187 | type = string 188 | default = null 189 | description = <<-EOT 190 | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. 191 | Possible values: `lower`, `title`, `upper`. 192 | Default value: `title`. 193 | EOT 194 | 195 | validation { 196 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 197 | error_message = "Allowed values: `lower`, `title`, `upper`." 198 | } 199 | } 200 | 201 | variable "label_value_case" { 202 | type = string 203 | default = null 204 | description = <<-EOT 205 | The letter case of output label values (also used in `tags` and `id`). 206 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 207 | Default value: `lower`. 208 | EOT 209 | 210 | validation { 211 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 212 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 213 | } 214 | } 215 | 216 | #### End of copy of cloudposse/terraform-null-label/variables.tf 217 | -------------------------------------------------------------------------------- /examples/autoscalinggroup/main.tf: -------------------------------------------------------------------------------- 1 | ################################ 2 | # terraform-null-label example # 3 | ################################ 4 | module "label" { 5 | source = "../../" 6 | 7 | context = module.this.context 8 | } 9 | 10 | ####################### 11 | # Launch template # 12 | ####################### 13 | resource "aws_launch_template" "default" { 14 | # terraform-null-label example used here: Set template name prefix 15 | name_prefix = "${module.label.id}-" 16 | image_id = data.aws_ami.amazon_linux.id 17 | instance_type = "t2.micro" 18 | instance_initiated_shutdown_behavior = "terminate" 19 | 20 | vpc_security_group_ids = [data.aws_security_group.default.id] 21 | 22 | monitoring { 23 | enabled = false 24 | } 25 | 26 | # terraform-null-label example used here: Set tags on everything that can be tagged 27 | tag_specifications { 28 | for_each = ["instance", "volume", "elastic-gpu", "spot-instance-request"] 29 | 30 | resource_type = each.value 31 | tags = module.label.tags 32 | } 33 | 34 | # Bridgecrew BC_AWS_GENERAL_26 35 | tags = module.label.tags 36 | 37 | # Bridgecrew compliance: Ensure Instance Metadata Service Version 1 is not enabled (BC_AWS_GENERAL_31) 38 | metadata_options { 39 | http_tokens = "required" 40 | } 41 | } 42 | 43 | ###################### 44 | # Autoscaling group # 45 | ###################### 46 | resource "aws_autoscaling_group" "default" { 47 | # terraform-null-label example used here: Set ASG name prefix 48 | name_prefix = "${module.label.id}-" 49 | vpc_zone_identifier = data.aws_subnets.all.ids 50 | max_size = "1" 51 | min_size = "1" 52 | desired_capacity = "1" 53 | 54 | launch_template { 55 | id = aws_launch_template.default.id 56 | version = "$Latest" 57 | } 58 | 59 | # terraform-null-label example used here: Set tags on ASG and EC2 Servers 60 | tags = module.label.tags_as_list_of_maps 61 | } 62 | 63 | ################################ 64 | # Provider # 65 | ################################ 66 | provider "aws" { 67 | region = "eu-west-1" 68 | 69 | # Make it faster by skipping unneeded checks here 70 | skip_get_ec2_platforms = true 71 | skip_metadata_api_check = true 72 | skip_region_validation = true 73 | skip_credentials_validation = true 74 | skip_requesting_account_id = true 75 | } 76 | 77 | ############################################################## 78 | # Data sources to get VPC, subnets and security group details 79 | ############################################################## 80 | data "aws_vpc" "default" { 81 | default = true 82 | } 83 | 84 | data "aws_subnets" "all" { 85 | filter { 86 | name = "vpc-id" 87 | values = [data.aws_vpc.default.id] 88 | } 89 | } 90 | 91 | data "aws_security_group" "default" { 92 | vpc_id = data.aws_vpc.default.id 93 | name = "default" 94 | } 95 | 96 | data "aws_ami" "amazon_linux" { 97 | most_recent = true 98 | 99 | owners = ["amazon"] 100 | 101 | filter { 102 | name = "name" 103 | 104 | values = [ 105 | "amzn-ami-hvm-*-x86_64-gp2", 106 | ] 107 | } 108 | 109 | filter { 110 | name = "owner-alias" 111 | 112 | values = [ 113 | "amazon", 114 | ] 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /examples/autoscalinggroup/outputs.tf: -------------------------------------------------------------------------------- 1 | # terraform-null-label example used here: Output list of tags applied in each format 2 | output "tags_as_list_of_maps" { 3 | value = module.label.tags_as_list_of_maps 4 | } 5 | 6 | output "tags" { 7 | value = module.label.tags 8 | } 9 | 10 | output "id" { 11 | value = module.label.id 12 | } 13 | -------------------------------------------------------------------------------- /examples/autoscalinggroup/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.13.0" 3 | } 4 | -------------------------------------------------------------------------------- /examples/complete/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | .terraform.lock.hcl 3 | **/.terraform/* 4 | *.tfstate 5 | *.tfstate.* 6 | *.tfvars 7 | !complete.auto.tfvars -------------------------------------------------------------------------------- /examples/complete/compatibility.tf: -------------------------------------------------------------------------------- 1 | #### 2 | # these tests ensure that new versions of null-label remain compatible and 3 | # interoperable with old versions of null-label. 4 | # 5 | # However, there is a known incompatibility we are not going to do anything about: 6 | # 7 | # The input regex_replace_chars specifies a regular expression, and characters matching it are removed 8 | # from labels/id elements. Prior to this release, if the delimiter itself matched the regular expression, 9 | # then the delimiter would be removed from the attributes portion of the id. This was not a problem 10 | # for most users, since the default delimiter was - (dash) and the default regex allowed dashes, but 11 | # if you customized the delimiter and/or regex, it mattered. So these 12 | # compatibility tests are required to allow the delimiter in the labels. 13 | 14 | module "source_v22_full" { 15 | source = "cloudposse/label/null" 16 | version = "0.22.1" 17 | 18 | enabled = true 19 | namespace = "CloudPosse" 20 | environment = "UAT" 21 | stage = "build" 22 | name = "Winston Churchroom" 23 | delimiter = "+" 24 | attributes = ["fire", "water"] 25 | 26 | tags = { 27 | City = "Dublin" 28 | Environment = "Private" 29 | } 30 | additional_tag_map = { 31 | propagate = true 32 | } 33 | label_order = ["name", "environment", "stage", "attributes"] 34 | regex_replace_chars = "/[^a-tv-zA-Z0-9+]/" # Eliminate "u" just to verify this is taking effect 35 | id_length_limit = 28 36 | } 37 | 38 | module "source_v22_empty" { 39 | source = "cloudposse/label/null" 40 | version = "0.22.1" 41 | 42 | stage = "STAGE" 43 | } 44 | 45 | module "source_v24_full" { 46 | source = "cloudposse/label/null" 47 | version = "0.24.1" 48 | 49 | enabled = true 50 | namespace = "CloudPosse" 51 | environment = "UAT" 52 | stage = "build" 53 | name = "Winston Churchroom" 54 | delimiter = "+" 55 | attributes = ["fire", "water"] 56 | 57 | tags = { 58 | City = "Dublin" 59 | Environment = "Private" 60 | } 61 | additional_tag_map = { 62 | propagate = true 63 | } 64 | label_order = ["name", "environment", "stage", "attributes"] 65 | regex_replace_chars = "/[^a-tv-zA-Z0-9+]/" # Eliminate "u" just to verify this is taking effect 66 | id_length_limit = 28 67 | 68 | label_key_case = "upper" 69 | label_value_case = "lower" 70 | } 71 | 72 | module "source_v24_empty" { 73 | source = "cloudposse/label/null" 74 | version = "0.24.1" 75 | 76 | stage = "STAGE" 77 | } 78 | 79 | # When testing the backward compatibility of supplying a new 80 | # context to an old module, it is not fair to use 81 | # the new features in the new module. 82 | module "source_v25_22_full" { 83 | source = "../.." 84 | 85 | enabled = true 86 | namespace = "CloudPosse" 87 | environment = "UAT" 88 | stage = "build" 89 | name = "Winston Churchroom" 90 | delimiter = "+" 91 | attributes = ["fire", "water"] 92 | 93 | tags = { 94 | City = "Dublin" 95 | Environment = "Private" 96 | } 97 | additional_tag_map = { 98 | propagate = true 99 | } 100 | label_order = ["name", "environment", "stage", "attributes"] 101 | # Need to add "+" to the regex in v0.22.1 due to a known issue: 102 | # the attributes string will have the delimiter stripped out 103 | # if the delimiter is selected by `regex_replace_chars`. 104 | # This was fixed in v0.24.1 105 | regex_replace_chars = "/[^a-tv-zA-Z0-9+]/" # Eliminate "u" just to verify this is taking effect 106 | id_length_limit = 28 107 | } 108 | 109 | module "source_v25_24_full" { 110 | source = "../.." 111 | regex_replace_chars = "/[^a-tv-zA-Z0-9]/" # Eliminate "u" just to verify this is taking effect 112 | 113 | label_key_case = "lower" 114 | label_value_case = "upper" 115 | 116 | context = module.source_v25_22_full.context 117 | } 118 | 119 | module "source_v25_empty" { 120 | source = "../.." 121 | 122 | stage = "STAGE" 123 | } 124 | 125 | module "compat_22_25_full" { 126 | source = "../.." 127 | context = module.source_v22_full.context 128 | } 129 | 130 | module "compat_24_25_full" { 131 | source = "../.." 132 | context = module.source_v24_full.context 133 | } 134 | 135 | module "compat_22_25_empty" { 136 | source = "../.." 137 | context = module.source_v22_empty.context 138 | } 139 | 140 | module "compat_24_25_empty" { 141 | source = "../.." 142 | context = module.source_v24_empty.context 143 | } 144 | 145 | module "compat_25_22_full" { 146 | source = "cloudposse/label/null" 147 | version = "0.22.1" 148 | 149 | # Known issue, additional_tag_map not taken from context 150 | additional_tag_map = module.source_v25_22_full.context.additional_tag_map 151 | 152 | context = module.source_v25_22_full.context 153 | } 154 | 155 | module "compat_25_24_full" { 156 | source = "cloudposse/label/null" 157 | version = "0.24.1" 158 | 159 | # Known issue, additional_tag_map not taken from context 160 | additional_tag_map = module.source_v25_22_full.context.additional_tag_map 161 | 162 | context = module.source_v25_24_full.context 163 | } 164 | 165 | module "compat_25_22_empty" { 166 | source = "cloudposse/label/null" 167 | version = "0.22.1" 168 | 169 | context = module.source_v25_empty.context 170 | } 171 | 172 | module "compat_25_24_empty" { 173 | source = "cloudposse/label/null" 174 | version = "0.24.1" 175 | 176 | context = module.source_v25_empty.context 177 | } 178 | 179 | module "compare_22_25_full" { 180 | source = "./module/compare" 181 | a = module.source_v22_full 182 | b = module.compat_22_25_full 183 | } 184 | 185 | output "compare_22_25_full" { 186 | value = module.compare_22_25_full 187 | } 188 | 189 | /* Uncomment this code to see how the fields differ 190 | output "source_22_full_id_full" { 191 | value = module.source_v22_full.id_full 192 | } 193 | output "compat_22_25_full_id_full" { 194 | value = module.compat_22_25_full.id_full 195 | } 196 | output "source_22_full_talm" { 197 | value = module.source_v22_full.tags_as_list_of_maps 198 | } 199 | output "compat_22_25_full_talm" { 200 | value = module.compat_22_25_full.tags_as_list_of_maps 201 | } 202 | */ 203 | 204 | module "compare_24_25_full" { 205 | source = "./module/compare" 206 | a = module.source_v24_full 207 | b = module.compat_24_25_full 208 | } 209 | 210 | output "compare_24_25_full" { 211 | value = module.compare_24_25_full 212 | } 213 | 214 | module "compare_22_25_empty" { 215 | source = "./module/compare" 216 | a = module.source_v22_empty 217 | b = module.compat_22_25_empty 218 | } 219 | 220 | output "compare_22_25_empty" { 221 | value = module.compare_22_25_empty 222 | } 223 | 224 | module "compare_24_25_empty" { 225 | source = "./module/compare" 226 | a = module.source_v24_empty 227 | b = module.compat_24_25_empty 228 | } 229 | 230 | output "compare_24_25_empty" { 231 | value = module.compare_24_25_empty 232 | } 233 | 234 | module "compare_25_22_full" { 235 | source = "./module/compare" 236 | a = module.source_v25_22_full 237 | b = module.compat_25_22_full 238 | } 239 | 240 | output "compare_25_22_full" { 241 | value = module.compare_25_22_full 242 | } 243 | 244 | module "compare_25_24_full" { 245 | source = "./module/compare" 246 | a = module.source_v25_24_full 247 | b = module.compat_25_24_full 248 | } 249 | 250 | output "compare_25_24_full" { 251 | value = module.compare_25_24_full 252 | } 253 | 254 | module "compare_25_22_empty" { 255 | source = "./module/compare" 256 | a = module.source_v25_empty 257 | b = module.compat_25_22_empty 258 | } 259 | 260 | output "compare_25_22_empty" { 261 | value = module.compare_25_22_empty 262 | } 263 | 264 | module "compare_25_24_empty" { 265 | source = "./module/compare" 266 | a = module.source_v25_empty 267 | b = module.compat_25_24_empty 268 | } 269 | 270 | output "compare_25_24_empty" { 271 | value = module.compare_25_24_empty 272 | } 273 | 274 | 275 | output "compatible" { 276 | value = ( 277 | module.compare_22_25_full.equal && 278 | module.compare_24_25_full.equal && 279 | module.compare_25_22_full.equal && 280 | module.compare_25_24_full.equal && 281 | module.compare_22_25_empty.equal && 282 | module.compare_24_25_empty.equal && 283 | module.compare_25_22_empty.equal && 284 | module.compare_25_24_empty.equal 285 | ) 286 | } -------------------------------------------------------------------------------- /examples/complete/complete.auto.tfvars: -------------------------------------------------------------------------------- 1 | namespace = "cp" 2 | environment = "uw2" 3 | stage = "prd" 4 | name = "null-label" 5 | 6 | delimiter = "" 7 | id_length_limit = 6 8 | 9 | label_key_case = "lower" 10 | label_value_case = "upper" 11 | -------------------------------------------------------------------------------- /examples/complete/context.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label 3 | # All other instances of this file should be a copy of that one 4 | # 5 | # 6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf 7 | # and then place it in your Terraform module to automatically get 8 | # Cloud Posse's standard configuration inputs suitable for passing 9 | # to Cloud Posse modules. 10 | # 11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf 12 | # 13 | # Modules should access the whole context as `module.this.context` 14 | # to get the input variables with nulls for defaults, 15 | # for example `context = module.this.context`, 16 | # and access individual variables as `module.this.`, 17 | # with final values filled in. 18 | # 19 | # For example, when using defaults, `module.this.context.delimiter` 20 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 21 | # 22 | 23 | module "this" { 24 | source = "../.." 25 | 26 | enabled = var.enabled 27 | namespace = var.namespace 28 | tenant = var.tenant 29 | environment = var.environment 30 | stage = var.stage 31 | name = var.name 32 | delimiter = var.delimiter 33 | attributes = var.attributes 34 | tags = var.tags 35 | additional_tag_map = var.additional_tag_map 36 | label_order = var.label_order 37 | regex_replace_chars = var.regex_replace_chars 38 | id_length_limit = var.id_length_limit 39 | label_key_case = var.label_key_case 40 | label_value_case = var.label_value_case 41 | descriptor_formats = var.descriptor_formats 42 | labels_as_tags = var.labels_as_tags 43 | 44 | context = var.context 45 | } 46 | 47 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 48 | 49 | variable "context" { 50 | type = any 51 | default = { 52 | enabled = true 53 | namespace = null 54 | tenant = null 55 | environment = null 56 | stage = null 57 | name = null 58 | delimiter = null 59 | attributes = [] 60 | tags = {} 61 | additional_tag_map = {} 62 | regex_replace_chars = null 63 | label_order = [] 64 | id_length_limit = null 65 | label_key_case = null 66 | label_value_case = null 67 | descriptor_formats = {} 68 | # Note: we have to use [] instead of null for unset lists due to 69 | # https://github.com/hashicorp/terraform/issues/28137 70 | # which was not fixed until Terraform 1.0.0, 71 | # but we want the default to be all the labels in `label_order` 72 | # and we want users to be able to prevent all tag generation 73 | # by setting `labels_as_tags` to `[]`, so we need 74 | # a different sentinel to indicate "default" 75 | labels_as_tags = ["unset"] 76 | } 77 | description = <<-EOT 78 | Single object for setting entire context at once. 79 | See description of individual variables for details. 80 | Leave string and numeric variables as `null` to use default value. 81 | Individual variable settings (non-null) override settings in context object, 82 | except for attributes, tags, and additional_tag_map, which are merged. 83 | EOT 84 | 85 | validation { 86 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 87 | error_message = "Allowed values: `lower`, `title`, `upper`." 88 | } 89 | 90 | validation { 91 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 92 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 93 | } 94 | } 95 | 96 | variable "enabled" { 97 | type = bool 98 | default = null 99 | description = "Set to false to prevent the module from creating any resources" 100 | } 101 | 102 | variable "namespace" { 103 | type = string 104 | default = null 105 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" 106 | } 107 | 108 | variable "tenant" { 109 | type = string 110 | default = null 111 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" 112 | } 113 | 114 | variable "environment" { 115 | type = string 116 | default = null 117 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" 118 | } 119 | 120 | variable "stage" { 121 | type = string 122 | default = null 123 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" 124 | } 125 | 126 | variable "name" { 127 | type = string 128 | default = null 129 | description = <<-EOT 130 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. 131 | This is the only ID element not also included as a `tag`. 132 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. 133 | EOT 134 | } 135 | 136 | variable "delimiter" { 137 | type = string 138 | default = null 139 | description = <<-EOT 140 | Delimiter to be used between ID elements. 141 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 142 | EOT 143 | } 144 | 145 | variable "attributes" { 146 | type = list(string) 147 | default = [] 148 | description = <<-EOT 149 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, 150 | in the order they appear in the list. New attributes are appended to the 151 | end of the list. The elements of the list are joined by the `delimiter` 152 | and treated as a single ID element. 153 | EOT 154 | } 155 | 156 | variable "labels_as_tags" { 157 | type = set(string) 158 | default = ["default"] 159 | description = <<-EOT 160 | Set of labels (ID elements) to include as tags in the `tags` output. 161 | Default is to include all labels. 162 | Tags with empty values will not be included in the `tags` output. 163 | Set to `[]` to suppress all generated tags. 164 | **Notes:** 165 | The value of the `name` tag, if included, will be the `id`, not the `name`. 166 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be 167 | changed in later chained modules. Attempts to change it will be silently ignored. 168 | EOT 169 | } 170 | 171 | variable "tags" { 172 | type = map(string) 173 | default = {} 174 | description = <<-EOT 175 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). 176 | Neither the tag keys nor the tag values will be modified by this module. 177 | EOT 178 | } 179 | 180 | variable "additional_tag_map" { 181 | type = map(string) 182 | default = {} 183 | description = <<-EOT 184 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. 185 | This is for some rare cases where resources want additional configuration of tags 186 | and therefore take a list of maps with tag key, value, and additional configuration. 187 | EOT 188 | } 189 | 190 | variable "label_order" { 191 | type = list(string) 192 | default = null 193 | description = <<-EOT 194 | The order in which the labels (ID elements) appear in the `id`. 195 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 196 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. 197 | EOT 198 | } 199 | 200 | variable "regex_replace_chars" { 201 | type = string 202 | default = null 203 | description = <<-EOT 204 | Terraform regular expression (regex) string. 205 | Characters matching the regex will be removed from the ID elements. 206 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 207 | EOT 208 | } 209 | 210 | variable "id_length_limit" { 211 | type = number 212 | default = null 213 | description = <<-EOT 214 | Limit `id` to this many characters (minimum 6). 215 | Set to `0` for unlimited length. 216 | Set to `null` for keep the existing setting, which defaults to `0`. 217 | Does not affect `id_full`. 218 | EOT 219 | validation { 220 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 221 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." 222 | } 223 | } 224 | 225 | variable "label_key_case" { 226 | type = string 227 | default = null 228 | description = <<-EOT 229 | Controls the letter case of the `tags` keys (label names) for tags generated by this module. 230 | Does not affect keys of tags passed in via the `tags` input. 231 | Possible values: `lower`, `title`, `upper`. 232 | Default value: `title`. 233 | EOT 234 | 235 | validation { 236 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 237 | error_message = "Allowed values: `lower`, `title`, `upper`." 238 | } 239 | } 240 | 241 | variable "label_value_case" { 242 | type = string 243 | default = null 244 | description = <<-EOT 245 | Controls the letter case of ID elements (labels) as included in `id`, 246 | set as tag values, and output by this module individually. 247 | Does not affect values of tags passed in via the `tags` input. 248 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 249 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. 250 | Default value: `lower`. 251 | EOT 252 | 253 | validation { 254 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 255 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 256 | } 257 | } 258 | 259 | variable "descriptor_formats" { 260 | type = any 261 | default = {} 262 | description = <<-EOT 263 | Describe additional descriptors to be output in the `descriptors` output map. 264 | Map of maps. Keys are names of descriptors. Values are maps of the form 265 | `{ 266 | format = string 267 | labels = list(string) 268 | }` 269 | (Type is `any` so the map values can later be enhanced to provide additional options.) 270 | `format` is a Terraform format string to be passed to the `format()` function. 271 | `labels` is a list of labels, in order, to pass to `format()` function. 272 | Label values will be normalized before being passed to `format()` so they will be 273 | identical to how they appear in `id`. 274 | Default is `{}` (`descriptors` output will be empty). 275 | EOT 276 | } 277 | 278 | #### End of copy of cloudposse/terraform-null-label/variables.tf 279 | -------------------------------------------------------------------------------- /examples/complete/descriptors.tf: -------------------------------------------------------------------------------- 1 | module "descriptors" { 2 | source = "../.." 3 | 4 | enabled = true 5 | tenant = "H.R.H" 6 | namespace = "CloudPosse" 7 | environment = "UAT" 8 | stage = "build" 9 | name = "Winston Churchroom" 10 | delimiter = "+" 11 | attributes = ["fire", "water"] 12 | 13 | tags = { 14 | City = "Dublin" 15 | Environment = "Private" 16 | } 17 | additional_tag_map = { 18 | propagate = true 19 | } 20 | label_order = ["name", "environment", "stage", "attributes"] 21 | regex_replace_chars = "/[^a-tv-zA-Z0-9+]/" # Eliminate "u" just to verify this is taking effect 22 | id_length_limit = 6 23 | 24 | descriptor_formats = { 25 | stack = { 26 | labels = ["tenant", "environment", "stage"] 27 | format = "%v-%v-%v" 28 | } 29 | account_name = { 30 | labels = ["stage", "tenant"] 31 | format = "%v-%v" 32 | } 33 | } 34 | } 35 | 36 | output "descriptor_stack" { 37 | value = module.descriptors.descriptors["stack"] 38 | } 39 | 40 | output "descriptor_account_name" { 41 | value = module.descriptors.descriptors["account_name"] 42 | } 43 | 44 | module "chained_descriptors" { 45 | source = "../.." 46 | 47 | context = module.descriptors.context 48 | } 49 | 50 | output "chained_descriptor_stack" { 51 | value = module.chained_descriptors.descriptors["stack"] 52 | } 53 | 54 | output "chained_descriptor_account_name" { 55 | value = module.chained_descriptors.descriptors["account_name"] 56 | } 57 | -------------------------------------------------------------------------------- /examples/complete/label1.tf: -------------------------------------------------------------------------------- 1 | module "label1" { 2 | source = "../../" 3 | namespace = "CloudPosse" 4 | tenant = "H.R.H" 5 | environment = "UAT" 6 | stage = "build" 7 | name = "Winston Churchroom" 8 | attributes = ["fire", "water", "earth", "air"] 9 | 10 | label_order = ["name", "tenant", "environment", "stage", "attributes"] 11 | 12 | tags = { 13 | "City" = "Dublin" 14 | "Environment" = "Private" 15 | } 16 | } 17 | 18 | output "label1" { 19 | value = { 20 | id = module.label1.id 21 | name = module.label1.name 22 | namespace = module.label1.namespace 23 | stage = module.label1.stage 24 | tenant = module.label1.tenant 25 | attributes = module.label1.attributes 26 | delimiter = module.label1.delimiter 27 | } 28 | } 29 | 30 | output "label1_tags" { 31 | value = module.label1.tags 32 | } 33 | 34 | output "label1_context" { 35 | value = module.label1.context 36 | } 37 | 38 | output "label1_normalized_context" { 39 | value = module.label1.normalized_context 40 | } 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/complete/label1t1.tf: -------------------------------------------------------------------------------- 1 | module "label1t1" { 2 | source = "../../" 3 | 4 | id_length_limit = 32 5 | 6 | context = module.label1.context 7 | } 8 | 9 | output "label1t1" { 10 | value = { 11 | id = module.label1t1.id 12 | id_full = module.label1t1.id_full 13 | } 14 | } 15 | 16 | output "label1t1_tags" { 17 | value = module.label1t1.tags 18 | } -------------------------------------------------------------------------------- /examples/complete/label1t2.tf: -------------------------------------------------------------------------------- 1 | module "label1t2" { 2 | source = "../../" 3 | 4 | id_length_limit = 33 5 | 6 | context = module.label1.context 7 | } 8 | 9 | output "label1t2" { 10 | value = { 11 | id = module.label1t2.id 12 | id_full = module.label1t2.id_full 13 | } 14 | } 15 | 16 | output "label1t2_tags" { 17 | value = module.label1t2.tags 18 | } -------------------------------------------------------------------------------- /examples/complete/label2.tf: -------------------------------------------------------------------------------- 1 | module "label2" { 2 | source = "../../" 3 | context = module.label1.context 4 | name = "Charlie" 5 | tenant = "" # setting to `null` would have no effect 6 | stage = "test" 7 | delimiter = "+" 8 | regex_replace_chars = "/[^a-zA-Z0-9-+]/" 9 | 10 | additional_tag_map = { 11 | propagate_at_launch = true 12 | additional_tag = "yes" 13 | } 14 | 15 | tags = { 16 | "City" = "London" 17 | "Environment" = "Public" 18 | } 19 | 20 | # Because this is chained from label1, labels_as_tags should have no effect 21 | labels_as_tags = ["stage"] 22 | } 23 | 24 | output "label2" { 25 | value = { 26 | id = module.label2.id 27 | name = module.label2.name 28 | namespace = module.label2.namespace 29 | stage = module.label2.stage 30 | tenant = module.label2.tenant 31 | attributes = module.label2.attributes 32 | delimiter = module.label2.delimiter 33 | } 34 | } 35 | 36 | output "label2_tags" { 37 | value = module.label2.tags 38 | } 39 | 40 | output "label2_tags_as_list_of_maps" { 41 | value = module.label2.tags_as_list_of_maps 42 | } 43 | 44 | output "label2_context" { 45 | value = module.label2.context 46 | } 47 | -------------------------------------------------------------------------------- /examples/complete/label3c.tf: -------------------------------------------------------------------------------- 1 | module "label3c" { 2 | source = "../../" 3 | name = "Starfish" 4 | stage = "release" 5 | context = module.label1.context 6 | delimiter = "." 7 | regex_replace_chars = "/[^-a-zA-Z0-9.]/" 8 | 9 | tags = { 10 | "Eat" = "Carrot" 11 | "Animal" = "Rabbit" 12 | } 13 | } 14 | 15 | output "label3c" { 16 | value = { 17 | id = module.label3c.id 18 | name = module.label3c.name 19 | namespace = module.label3c.namespace 20 | stage = module.label3c.stage 21 | tenant = module.label3c.tenant 22 | attributes = module.label3c.attributes 23 | delimiter = module.label3c.delimiter 24 | } 25 | } 26 | 27 | output "label3c_tags" { 28 | value = module.label3c.tags 29 | } 30 | 31 | output "label3c_context" { 32 | value = module.label3c.context 33 | } 34 | 35 | output "label3c_normalized_context" { 36 | value = module.label3c.normalized_context 37 | } 38 | -------------------------------------------------------------------------------- /examples/complete/label3n.tf: -------------------------------------------------------------------------------- 1 | module "label3n" { 2 | source = "../../" 3 | name = "Starfish" 4 | stage = "release" 5 | context = module.label1.normalized_context 6 | delimiter = "." 7 | regex_replace_chars = "/[^-a-zA-Z0-9.]/" 8 | 9 | tags = { 10 | "Eat" = "Carrot" 11 | "Animal" = "Rabbit" 12 | } 13 | } 14 | 15 | output "label3n" { 16 | value = { 17 | id = module.label3n.id 18 | name = module.label3n.name 19 | namespace = module.label3n.namespace 20 | stage = module.label3n.stage 21 | tenant = module.label3n.tenant 22 | attributes = module.label3n.attributes 23 | delimiter = module.label3n.delimiter 24 | } 25 | } 26 | 27 | output "label3n_tags" { 28 | value = module.label3n.tags 29 | } 30 | 31 | output "label3n_context" { 32 | value = module.label3n.context 33 | } 34 | 35 | output "label3n_normalized_context" { 36 | value = module.label3n.normalized_context 37 | } 38 | -------------------------------------------------------------------------------- /examples/complete/label4.tf: -------------------------------------------------------------------------------- 1 | module "label4" { 2 | source = "../../" 3 | namespace = "CloudPosse" 4 | environment = "UAT" 5 | name = "Example Cluster" 6 | attributes = ["big", "fat", "honking", "cluster"] 7 | delimiter = "-" 8 | 9 | label_order = ["namespace", "stage", "environment", "attributes"] 10 | 11 | tags = { 12 | "City" = "Dublin" 13 | "Environment" = "Private" 14 | } 15 | } 16 | 17 | output "label4" { 18 | value = { 19 | id = module.label4.id 20 | name = module.label4.name 21 | namespace = module.label4.namespace 22 | stage = module.label4.stage 23 | attributes = module.label4.attributes 24 | delimiter = module.label4.delimiter 25 | } 26 | } 27 | 28 | output "label4_tags" { 29 | value = module.label4.tags 30 | } 31 | 32 | output "label4_context" { 33 | value = module.label4.context 34 | } 35 | -------------------------------------------------------------------------------- /examples/complete/label5.tf: -------------------------------------------------------------------------------- 1 | module "label5" { 2 | source = "../../" 3 | enabled = false 4 | namespace = "eg" 5 | environment = "demo" 6 | name = "blue" 7 | attributes = ["cluster"] 8 | delimiter = "-" 9 | 10 | label_order = ["namespace", "stage", "environment", "attributes"] 11 | 12 | tags = { 13 | } 14 | } 15 | 16 | output "label5" { 17 | value = { 18 | id = module.label5.id 19 | name = module.label5.name 20 | namespace = module.label5.namespace 21 | stage = module.label5.stage 22 | attributes = module.label5.attributes 23 | delimiter = module.label5.delimiter 24 | } 25 | } 26 | 27 | output "label5_tags" { 28 | value = module.label5.tags 29 | } 30 | 31 | output "label5_context" { 32 | value = module.label5.context 33 | } 34 | -------------------------------------------------------------------------------- /examples/complete/label6f.tf: -------------------------------------------------------------------------------- 1 | module "label6f" { 2 | source = "../../" 3 | 4 | delimiter = "~" 5 | id_length_limit = 0 6 | 7 | # Use values from tfvars 8 | context = module.this.context 9 | } 10 | 11 | output "label6f" { 12 | value = { 13 | id = module.label6f.id 14 | id_full = module.label6f.id_full 15 | } 16 | } 17 | 18 | output "label6f_tags" { 19 | value = module.label6f.tags 20 | } -------------------------------------------------------------------------------- /examples/complete/label6t.tf: -------------------------------------------------------------------------------- 1 | module "label6t" { 2 | source = "../../" 3 | 4 | # Use values from tfvars, 5 | # specifically: complete.auto.tfvars 6 | context = module.this.context 7 | } 8 | 9 | output "label6t" { 10 | value = { 11 | id = module.label6t.id 12 | id_full = module.label6t.id_full 13 | id_length_limit = module.this.context.id_length_limit 14 | } 15 | } 16 | 17 | output "label6t_tags" { 18 | value = module.label6t.tags 19 | } -------------------------------------------------------------------------------- /examples/complete/label7.tf: -------------------------------------------------------------------------------- 1 | module "label7a" { 2 | source = "../../" 3 | enabled = true 4 | namespace = "eg" 5 | environment = "demo" 6 | name = "blue" 7 | attributes = ["cluster"] 8 | delimiter = "-" 9 | 10 | tags = { 11 | } 12 | } 13 | 14 | module "label7" { 15 | source = "../../" 16 | 17 | attributes = ["nodegroup"] 18 | 19 | context = module.label7a.context 20 | } 21 | 22 | 23 | output "label7" { 24 | value = { 25 | id = module.label7.id 26 | name = module.label7.name 27 | namespace = module.label7.namespace 28 | stage = module.label7.stage 29 | attributes = module.label7.attributes 30 | delimiter = module.label7.delimiter 31 | } 32 | } 33 | 34 | output "label7_id" { 35 | value = module.label7.id 36 | } 37 | 38 | output "label7_attributes" { 39 | value = module.label7.attributes 40 | } 41 | 42 | output "label7_context" { 43 | value = module.label7.context 44 | } 45 | -------------------------------------------------------------------------------- /examples/complete/label8d.tf: -------------------------------------------------------------------------------- 1 | module "label8d" { 2 | source = "../../" 3 | 4 | enabled = true 5 | namespace = "eg" 6 | environment = "demo" 7 | # Verify that an empty "name" will not suppress the "Name" tag 8 | tenant = "blue" 9 | attributes = ["cluster"] 10 | delimiter = "-" 11 | 12 | tags = { 13 | "kubernetes.io/cluster/" = "shared" 14 | } 15 | 16 | label_order = ["namespace", "environment", "tenant", "attributes"] 17 | 18 | # Verify an empty "stage" label will not be exported as a tag 19 | labels_as_tags = ["environment", "name", "attributes", "stage"] 20 | } 21 | 22 | module "label8d_chained" { 23 | source = "../../" 24 | 25 | # Override should fail, should get same tags as label8d 26 | labels_as_tags = ["namespace"] 27 | 28 | context = module.label8d.context 29 | } 30 | 31 | module "label8d_context" { 32 | source = "../../" 33 | 34 | context = module.label8d.context 35 | } 36 | 37 | output "label8d_context_id" { 38 | value = module.label8d_context.id 39 | } 40 | 41 | output "label8d_context_context" { 42 | value = module.label8d_context.context 43 | } 44 | 45 | output "label8d_context_tags" { 46 | value = module.label8d_context.tags 47 | } 48 | 49 | output "label8d_id" { 50 | value = module.label8d.id 51 | } 52 | 53 | output "label8d_context" { 54 | value = module.label8d.context 55 | } 56 | 57 | output "label8d_tags" { 58 | value = module.label8d.tags 59 | } 60 | 61 | output "label8d_chained_context_labels_as_tags" { 62 | value = join("-", sort(tolist(module.label8d_chained.context.labels_as_tags))) 63 | } -------------------------------------------------------------------------------- /examples/complete/label8dcd.tf: -------------------------------------------------------------------------------- 1 | module "label8dcd" { 2 | source = "../../" 3 | 4 | enabled = true 5 | namespace = "eg" 6 | environment = "demo" 7 | name = "blue" 8 | attributes = ["cluster"] 9 | delimiter = "x" 10 | } 11 | 12 | module "label8dcd_context" { 13 | source = "../../" 14 | 15 | context = module.label8dcd.context 16 | } 17 | 18 | output "label8dcd_context_id" { 19 | value = module.label8dcd_context.id 20 | } 21 | 22 | output "label8dcd_id" { 23 | value = module.label8dcd.id 24 | } 25 | -------------------------------------------------------------------------------- /examples/complete/label8dnd.tf: -------------------------------------------------------------------------------- 1 | module "label8dnd" { 2 | source = "../../" 3 | 4 | enabled = true 5 | namespace = "eg" 6 | environment = "demo" 7 | name = "blue" 8 | attributes = ["cluster"] 9 | delimiter = "" 10 | } 11 | 12 | module "label8dnd_context" { 13 | source = "../../" 14 | 15 | context = module.label8dnd.context 16 | } 17 | 18 | output "label8dnd_context_id" { 19 | value = module.label8dnd_context.id 20 | } 21 | 22 | output "label8dnd_id" { 23 | value = module.label8dnd.id 24 | } 25 | -------------------------------------------------------------------------------- /examples/complete/label8l.tf: -------------------------------------------------------------------------------- 1 | module "label8l" { 2 | source = "../../" 3 | enabled = true 4 | namespace = "eg" 5 | environment = "demo" 6 | name = "blue" 7 | attributes = ["cluster"] 8 | delimiter = "-" 9 | label_key_case = "lower" 10 | label_value_case = "lower" 11 | 12 | tags = { 13 | "kubernetes.io/cluster/" = "shared" 14 | "upperTEST" = "testUPPER" 15 | } 16 | } 17 | 18 | module "label8l_context" { 19 | source = "../../" 20 | 21 | context = module.label8l.context 22 | } 23 | 24 | output "label8l_context_id" { 25 | value = module.label8l_context.id 26 | } 27 | 28 | output "label8l_context_context" { 29 | value = module.label8l_context.context 30 | } 31 | 32 | output "label8l_context_tags" { 33 | value = module.label8l_context.tags 34 | } 35 | 36 | output "label8l_id" { 37 | value = module.label8l.id 38 | } 39 | 40 | output "label8l_context" { 41 | value = module.label8l.context 42 | } 43 | 44 | output "label8l_tags" { 45 | value = module.label8l.tags 46 | } 47 | -------------------------------------------------------------------------------- /examples/complete/label8n.tf: -------------------------------------------------------------------------------- 1 | module "label8n" { 2 | source = "../../" 3 | 4 | enabled = true 5 | namespace = "EG" 6 | environment = "demo" 7 | name = "blue" 8 | attributes = ["eks", "ClusteR"] 9 | delimiter = "-" 10 | label_value_case = "none" 11 | 12 | tags = { 13 | "kubernetes.io/cluster/" = "shared" 14 | } 15 | } 16 | 17 | module "label8n_context" { 18 | source = "../../" 19 | 20 | context = module.label8n.context 21 | } 22 | 23 | output "label8n_context_id" { 24 | value = module.label8n_context.id 25 | } 26 | 27 | output "label8n_context_context" { 28 | value = module.label8n_context.context 29 | } 30 | 31 | output "label8n_context_tags" { 32 | value = module.label8n_context.tags 33 | } 34 | 35 | output "label8n_id" { 36 | value = module.label8n.id 37 | } 38 | 39 | output "label8n_context" { 40 | value = module.label8n.context 41 | } 42 | 43 | output "label8n_tags" { 44 | value = module.label8n.tags 45 | } 46 | -------------------------------------------------------------------------------- /examples/complete/label8t.tf: -------------------------------------------------------------------------------- 1 | module "label8t" { 2 | source = "../../" 3 | enabled = true 4 | namespace = "eg" 5 | environment = "demo" 6 | name = "blue" 7 | attributes = ["EKS", "cluster"] 8 | delimiter = "-" 9 | label_key_case = "title" 10 | label_value_case = "title" 11 | 12 | tags = { 13 | "kubernetes.io/cluster/" = "shared" 14 | } 15 | } 16 | 17 | module "label8t_context" { 18 | source = "../../" 19 | 20 | context = module.label8t.context 21 | } 22 | 23 | output "label8t_context_id" { 24 | value = module.label8t_context.id 25 | } 26 | 27 | output "label8t_context_context" { 28 | value = module.label8t_context.context 29 | } 30 | 31 | output "label8t_context_tags" { 32 | value = module.label8t_context.tags 33 | } 34 | 35 | output "label8t_id" { 36 | value = module.label8t.id 37 | } 38 | 39 | output "label8t_context" { 40 | value = module.label8t.context 41 | } 42 | 43 | output "label8t_tags" { 44 | value = module.label8t.tags 45 | } 46 | -------------------------------------------------------------------------------- /examples/complete/label8u.tf: -------------------------------------------------------------------------------- 1 | module "label8u" { 2 | source = "../../" 3 | enabled = true 4 | namespace = "eg" 5 | environment = "demo" 6 | name = "blue" 7 | attributes = ["cluster"] 8 | delimiter = "-" 9 | label_key_case = "upper" 10 | label_value_case = "upper" 11 | 12 | tags = { 13 | "kubernetes.io/cluster/" = "shared" 14 | } 15 | } 16 | 17 | module "label8u_context" { 18 | source = "../../" 19 | 20 | context = module.label8u.context 21 | } 22 | 23 | output "label8u_context_id" { 24 | value = module.label8u_context.id 25 | } 26 | 27 | output "label8u_context_context" { 28 | value = module.label8u_context.context 29 | } 30 | 31 | // debug 32 | output "label8u_context_normalized_context" { 33 | value = module.label8u_context.normalized_context 34 | } 35 | 36 | output "label8u_context_tags" { 37 | value = module.label8u_context.tags 38 | } 39 | 40 | output "label8u_id" { 41 | value = module.label8u.id 42 | } 43 | 44 | output "label8u_context" { 45 | value = module.label8u.context 46 | } 47 | 48 | output "label8u_tags" { 49 | value = module.label8u.tags 50 | } 51 | -------------------------------------------------------------------------------- /examples/complete/module/compare/compare.tf: -------------------------------------------------------------------------------- 1 | # This module compares the outputs of 2 instances of null-label and determines 2 | # whether or not they are equivalent. Used to detect when changes to new 3 | # versions cause an unintended difference in output/behavior 4 | # that would break compatibility. 5 | 6 | 7 | variable "a" { 8 | type = any 9 | } 10 | 11 | variable "b" { 12 | type = any 13 | } 14 | 15 | locals { 16 | equal_id = var.a.id == var.b.id 17 | equal_id_full = var.a.id_full == var.b.id_full 18 | equal_tags_as_list_of_maps = jsonencode(var.a.tags_as_list_of_maps) == jsonencode(var.b.tags_as_list_of_maps) 19 | equal = local.equal_id && local.equal_id_full && local.equal_normalized_context && local.equal_tags_as_list_of_maps 20 | 21 | context_keys = setintersection(keys(var.a.normalized_context), keys(var.b.normalized_context)) 22 | a_context_compare = { for k in local.context_keys : k => var.a.normalized_context[k] } 23 | b_context_compare = { for k in local.context_keys : k => var.b.normalized_context[k] } 24 | equal_normalized_context = jsonencode(local.a_context_compare) == jsonencode(local.b_context_compare) 25 | 26 | } 27 | 28 | output "equal" { 29 | value = local.equal 30 | } 31 | 32 | output "equal_id" { 33 | value = local.equal_id 34 | } 35 | 36 | output "equal_id_full" { 37 | value = local.equal_id_full 38 | } 39 | 40 | output "equal_normalized_context" { 41 | value = local.equal_normalized_context 42 | } 43 | 44 | output "equal_tags_as_list_of_maps" { 45 | value = local.equal_tags_as_list_of_maps 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/complete/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.13.0" 3 | } 4 | -------------------------------------------------------------------------------- /exports/context.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label 3 | # All other instances of this file should be a copy of that one 4 | # 5 | # 6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf 7 | # and then place it in your Terraform module to automatically get 8 | # Cloud Posse's standard configuration inputs suitable for passing 9 | # to Cloud Posse modules. 10 | # 11 | # curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf 12 | # 13 | # Modules should access the whole context as `module.this.context` 14 | # to get the input variables with nulls for defaults, 15 | # for example `context = module.this.context`, 16 | # and access individual variables as `module.this.`, 17 | # with final values filled in. 18 | # 19 | # For example, when using defaults, `module.this.context.delimiter` 20 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 21 | # 22 | 23 | module "this" { 24 | source = "cloudposse/label/null" 25 | version = "0.25.0" # requires Terraform >= 0.13.0 26 | 27 | enabled = var.enabled 28 | namespace = var.namespace 29 | tenant = var.tenant 30 | environment = var.environment 31 | stage = var.stage 32 | name = var.name 33 | delimiter = var.delimiter 34 | attributes = var.attributes 35 | tags = var.tags 36 | additional_tag_map = var.additional_tag_map 37 | label_order = var.label_order 38 | regex_replace_chars = var.regex_replace_chars 39 | id_length_limit = var.id_length_limit 40 | label_key_case = var.label_key_case 41 | label_value_case = var.label_value_case 42 | descriptor_formats = var.descriptor_formats 43 | labels_as_tags = var.labels_as_tags 44 | 45 | context = var.context 46 | } 47 | 48 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 49 | 50 | variable "context" { 51 | type = any 52 | default = { 53 | enabled = true 54 | namespace = null 55 | tenant = null 56 | environment = null 57 | stage = null 58 | name = null 59 | delimiter = null 60 | attributes = [] 61 | tags = {} 62 | additional_tag_map = {} 63 | regex_replace_chars = null 64 | label_order = [] 65 | id_length_limit = null 66 | label_key_case = null 67 | label_value_case = null 68 | descriptor_formats = {} 69 | # Note: we have to use [] instead of null for unset lists due to 70 | # https://github.com/hashicorp/terraform/issues/28137 71 | # which was not fixed until Terraform 1.0.0, 72 | # but we want the default to be all the labels in `label_order` 73 | # and we want users to be able to prevent all tag generation 74 | # by setting `labels_as_tags` to `[]`, so we need 75 | # a different sentinel to indicate "default" 76 | labels_as_tags = ["unset"] 77 | } 78 | description = <<-EOT 79 | Single object for setting entire context at once. 80 | See description of individual variables for details. 81 | Leave string and numeric variables as `null` to use default value. 82 | Individual variable settings (non-null) override settings in context object, 83 | except for attributes, tags, and additional_tag_map, which are merged. 84 | EOT 85 | 86 | validation { 87 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 88 | error_message = "Allowed values: `lower`, `title`, `upper`." 89 | } 90 | 91 | validation { 92 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 93 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 94 | } 95 | } 96 | 97 | variable "enabled" { 98 | type = bool 99 | default = null 100 | description = "Set to false to prevent the module from creating any resources" 101 | } 102 | 103 | variable "namespace" { 104 | type = string 105 | default = null 106 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" 107 | } 108 | 109 | variable "tenant" { 110 | type = string 111 | default = null 112 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" 113 | } 114 | 115 | variable "environment" { 116 | type = string 117 | default = null 118 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" 119 | } 120 | 121 | variable "stage" { 122 | type = string 123 | default = null 124 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" 125 | } 126 | 127 | variable "name" { 128 | type = string 129 | default = null 130 | description = <<-EOT 131 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. 132 | This is the only ID element not also included as a `tag`. 133 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. 134 | EOT 135 | } 136 | 137 | variable "delimiter" { 138 | type = string 139 | default = null 140 | description = <<-EOT 141 | Delimiter to be used between ID elements. 142 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 143 | EOT 144 | } 145 | 146 | variable "attributes" { 147 | type = list(string) 148 | default = [] 149 | description = <<-EOT 150 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, 151 | in the order they appear in the list. New attributes are appended to the 152 | end of the list. The elements of the list are joined by the `delimiter` 153 | and treated as a single ID element. 154 | EOT 155 | } 156 | 157 | variable "labels_as_tags" { 158 | type = set(string) 159 | default = ["default"] 160 | description = <<-EOT 161 | Set of labels (ID elements) to include as tags in the `tags` output. 162 | Default is to include all labels. 163 | Tags with empty values will not be included in the `tags` output. 164 | Set to `[]` to suppress all generated tags. 165 | **Notes:** 166 | The value of the `name` tag, if included, will be the `id`, not the `name`. 167 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be 168 | changed in later chained modules. Attempts to change it will be silently ignored. 169 | EOT 170 | } 171 | 172 | variable "tags" { 173 | type = map(string) 174 | default = {} 175 | description = <<-EOT 176 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). 177 | Neither the tag keys nor the tag values will be modified by this module. 178 | EOT 179 | } 180 | 181 | variable "additional_tag_map" { 182 | type = map(string) 183 | default = {} 184 | description = <<-EOT 185 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. 186 | This is for some rare cases where resources want additional configuration of tags 187 | and therefore take a list of maps with tag key, value, and additional configuration. 188 | EOT 189 | } 190 | 191 | variable "label_order" { 192 | type = list(string) 193 | default = null 194 | description = <<-EOT 195 | The order in which the labels (ID elements) appear in the `id`. 196 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 197 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. 198 | EOT 199 | } 200 | 201 | variable "regex_replace_chars" { 202 | type = string 203 | default = null 204 | description = <<-EOT 205 | Terraform regular expression (regex) string. 206 | Characters matching the regex will be removed from the ID elements. 207 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 208 | EOT 209 | } 210 | 211 | variable "id_length_limit" { 212 | type = number 213 | default = null 214 | description = <<-EOT 215 | Limit `id` to this many characters (minimum 6). 216 | Set to `0` for unlimited length. 217 | Set to `null` for keep the existing setting, which defaults to `0`. 218 | Does not affect `id_full`. 219 | EOT 220 | validation { 221 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 222 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." 223 | } 224 | } 225 | 226 | variable "label_key_case" { 227 | type = string 228 | default = null 229 | description = <<-EOT 230 | Controls the letter case of the `tags` keys (label names) for tags generated by this module. 231 | Does not affect keys of tags passed in via the `tags` input. 232 | Possible values: `lower`, `title`, `upper`. 233 | Default value: `title`. 234 | EOT 235 | 236 | validation { 237 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 238 | error_message = "Allowed values: `lower`, `title`, `upper`." 239 | } 240 | } 241 | 242 | variable "label_value_case" { 243 | type = string 244 | default = null 245 | description = <<-EOT 246 | Controls the letter case of ID elements (labels) as included in `id`, 247 | set as tag values, and output by this module individually. 248 | Does not affect values of tags passed in via the `tags` input. 249 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 250 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. 251 | Default value: `lower`. 252 | EOT 253 | 254 | validation { 255 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 256 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 257 | } 258 | } 259 | 260 | variable "descriptor_formats" { 261 | type = any 262 | default = {} 263 | description = <<-EOT 264 | Describe additional descriptors to be output in the `descriptors` output map. 265 | Map of maps. Keys are names of descriptors. Values are maps of the form 266 | `{ 267 | format = string 268 | labels = list(string) 269 | }` 270 | (Type is `any` so the map values can later be enhanced to provide additional options.) 271 | `format` is a Terraform format string to be passed to the `format()` function. 272 | `labels` is a list of labels, in order, to pass to `format()` function. 273 | Label values will be normalized before being passed to `format()` so they will be 274 | identical to how they appear in `id`. 275 | Default is `{}` (`descriptors` output will be empty). 276 | EOT 277 | } 278 | 279 | #### End of copy of cloudposse/terraform-null-label/variables.tf 280 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | 3 | defaults = { 4 | # The `tenant` label was introduced in v0.25.0. To preserve backward compatibility, or, really, to ensure 5 | # that people using the `tenant` label are alerted that it was not previously supported if they try to 6 | # use it in an older version, it is not included by default. 7 | label_order = ["namespace", "environment", "stage", "name", "attributes"] 8 | regex_replace_chars = "/[^-a-zA-Z0-9]/" 9 | delimiter = "-" 10 | replacement = "" 11 | id_length_limit = 0 12 | id_hash_length = 5 13 | label_key_case = "title" 14 | label_value_case = "lower" 15 | 16 | # The default value of labels_as_tags cannot be included in this 17 | # defaults` map because it creates a circular dependency 18 | } 19 | 20 | default_labels_as_tags = keys(local.tags_context) 21 | # Unlike other inputs, the first setting of `labels_as_tags` cannot be later overridden. However, 22 | # we still have to pass the `input` map as the context to the next module. So we need to distinguish 23 | # between the first setting of var.labels_as_tags == null as meaning set the default and do not change 24 | # it later, versus later settings of var.labels_as_tags that should be ignored. So, we make the 25 | # default value in context be "unset", meaning it can be changed, but when it is unset and 26 | # var.labels_as_tags is null, we change it to "default". Once it is set to "default" we will 27 | # not allow it to be changed again, but of course we have to detect "default" and replace it 28 | # with local.default_labels_as_tags when we go to use it. 29 | # 30 | # We do not want to use null as default or unset, because Terraform has issues with 31 | # the value of an object field being null in some places and [] in others. 32 | # We do not want to use [] as default or unset because that is actually a valid setting 33 | # that we want to have override the default. 34 | # 35 | # To determine whether that context.labels_as_tags is not set, 36 | # we have to cover 2 cases: 1) context does not have a labels_as_tags key, 2) it is present and set to ["unset"] 37 | context_labels_as_tags_is_unset = try(contains(var.context.labels_as_tags, "unset"), true) 38 | 39 | # So far, we have decided not to allow overriding replacement or id_hash_length 40 | replacement = local.defaults.replacement 41 | id_hash_length = local.defaults.id_hash_length 42 | 43 | # The values provided by variables supersede the values inherited from the context object, 44 | # except for tags and attributes which are merged. 45 | input = { 46 | # It would be nice to use coalesce here, but we cannot, because it 47 | # is an error for all the arguments to coalesce to be empty. 48 | enabled = var.enabled == null ? var.context.enabled : var.enabled 49 | namespace = var.namespace == null ? var.context.namespace : var.namespace 50 | # tenant was introduced in v0.25.0, prior context versions do not have it 51 | tenant = var.tenant == null ? lookup(var.context, "tenant", null) : var.tenant 52 | environment = var.environment == null ? var.context.environment : var.environment 53 | stage = var.stage == null ? var.context.stage : var.stage 54 | name = var.name == null ? var.context.name : var.name 55 | delimiter = var.delimiter == null ? var.context.delimiter : var.delimiter 56 | # modules tack on attributes (passed by var) to the end of the list (passed by context) 57 | attributes = compact(distinct(concat(coalesce(var.context.attributes, []), coalesce(var.attributes, [])))) 58 | tags = merge(var.context.tags, var.tags) 59 | 60 | additional_tag_map = merge(var.context.additional_tag_map, var.additional_tag_map) 61 | label_order = var.label_order == null ? var.context.label_order : var.label_order 62 | regex_replace_chars = var.regex_replace_chars == null ? var.context.regex_replace_chars : var.regex_replace_chars 63 | id_length_limit = var.id_length_limit == null ? var.context.id_length_limit : var.id_length_limit 64 | label_key_case = var.label_key_case == null ? lookup(var.context, "label_key_case", null) : var.label_key_case 65 | label_value_case = var.label_value_case == null ? lookup(var.context, "label_value_case", null) : var.label_value_case 66 | 67 | descriptor_formats = merge(lookup(var.context, "descriptor_formats", {}), var.descriptor_formats) 68 | labels_as_tags = local.context_labels_as_tags_is_unset ? var.labels_as_tags : var.context.labels_as_tags 69 | } 70 | 71 | 72 | enabled = local.input.enabled 73 | regex_replace_chars = coalesce(local.input.regex_replace_chars, local.defaults.regex_replace_chars) 74 | 75 | # string_label_names are names of inputs that are strings (not list of strings) used as labels 76 | string_label_names = ["namespace", "tenant", "environment", "stage", "name"] 77 | normalized_labels = { for k in local.string_label_names : k => 78 | local.input[k] == null ? "" : replace(local.input[k], local.regex_replace_chars, local.replacement) 79 | } 80 | normalized_attributes = compact(distinct([for v in local.input.attributes : replace(v, local.regex_replace_chars, local.replacement)])) 81 | 82 | formatted_labels = { for k in local.string_label_names : k => local.label_value_case == "none" ? local.normalized_labels[k] : 83 | local.label_value_case == "title" ? title(lower(local.normalized_labels[k])) : 84 | local.label_value_case == "upper" ? upper(local.normalized_labels[k]) : lower(local.normalized_labels[k]) 85 | } 86 | 87 | attributes = compact(distinct([ 88 | for v in local.normalized_attributes : (local.label_value_case == "none" ? v : 89 | local.label_value_case == "title" ? title(lower(v)) : 90 | local.label_value_case == "upper" ? upper(v) : lower(v)) 91 | ])) 92 | 93 | namespace = local.formatted_labels["namespace"] 94 | tenant = local.formatted_labels["tenant"] 95 | environment = local.formatted_labels["environment"] 96 | stage = local.formatted_labels["stage"] 97 | name = local.formatted_labels["name"] 98 | 99 | delimiter = local.input.delimiter == null ? local.defaults.delimiter : local.input.delimiter 100 | label_order = local.input.label_order == null ? local.defaults.label_order : coalescelist(local.input.label_order, local.defaults.label_order) 101 | id_length_limit = local.input.id_length_limit == null ? local.defaults.id_length_limit : local.input.id_length_limit 102 | label_key_case = local.input.label_key_case == null ? local.defaults.label_key_case : local.input.label_key_case 103 | label_value_case = local.input.label_value_case == null ? local.defaults.label_value_case : local.input.label_value_case 104 | 105 | # labels_as_tags is an exception to the rule that input vars override context values (see above) 106 | labels_as_tags = contains(local.input.labels_as_tags, "default") ? local.default_labels_as_tags : local.input.labels_as_tags 107 | 108 | # Just for standardization and completeness 109 | descriptor_formats = local.input.descriptor_formats 110 | 111 | additional_tag_map = merge(var.context.additional_tag_map, var.additional_tag_map) 112 | 113 | tags = merge(local.generated_tags, local.input.tags) 114 | 115 | tags_as_list_of_maps = flatten([ 116 | for key in keys(local.tags) : merge( 117 | { 118 | key = key 119 | value = local.tags[key] 120 | }, local.additional_tag_map) 121 | ]) 122 | 123 | tags_context = { 124 | namespace = local.namespace 125 | tenant = local.tenant 126 | environment = local.environment 127 | stage = local.stage 128 | # For AWS we need `Name` to be disambiguated since it has a special meaning 129 | name = local.id 130 | attributes = local.id_context.attributes 131 | } 132 | 133 | generated_tags = { 134 | for l in setintersection(keys(local.tags_context), local.labels_as_tags) : 135 | local.label_key_case == "upper" ? upper(l) : ( 136 | local.label_key_case == "lower" ? lower(l) : title(lower(l)) 137 | ) => local.tags_context[l] if length(local.tags_context[l]) > 0 138 | } 139 | 140 | id_context = { 141 | namespace = local.namespace 142 | tenant = local.tenant 143 | environment = local.environment 144 | stage = local.stage 145 | name = local.name 146 | attributes = join(local.delimiter, local.attributes) 147 | } 148 | 149 | labels = [for l in local.label_order : local.id_context[l] if length(local.id_context[l]) > 0] 150 | 151 | id_full = join(local.delimiter, local.labels) 152 | # Create a truncated ID if needed 153 | delimiter_length = length(local.delimiter) 154 | # Calculate length of normal part of ID, leaving room for delimiter and hash 155 | id_truncated_length_limit = local.id_length_limit - (local.id_hash_length + local.delimiter_length) 156 | # Truncate the ID and ensure a single (not double) trailing delimiter 157 | id_truncated = local.id_truncated_length_limit <= 0 ? "" : "${trimsuffix(substr(local.id_full, 0, local.id_truncated_length_limit), local.delimiter)}${local.delimiter}" 158 | # Support usages that disallow numeric characters. Would prefer tr 0-9 q-z but Terraform does not support it. 159 | # Probably would have been better to take the hash of only the characters being removed, 160 | # so identical removed strings would produce identical hashes, but it is not worth breaking existing IDs for. 161 | id_hash_plus = "${md5(local.id_full)}qrstuvwxyz" 162 | id_hash_case = local.label_value_case == "title" ? title(local.id_hash_plus) : local.label_value_case == "upper" ? upper(local.id_hash_plus) : local.label_value_case == "lower" ? lower(local.id_hash_plus) : local.id_hash_plus 163 | id_hash = replace(local.id_hash_case, local.regex_replace_chars, local.replacement) 164 | # Create the short ID by adding a hash to the end of the truncated ID 165 | id_short = substr("${local.id_truncated}${local.id_hash}", 0, local.id_length_limit) 166 | id = local.id_length_limit != 0 && length(local.id_full) > local.id_length_limit ? local.id_short : local.id_full 167 | 168 | 169 | # Context of this label to pass to other label modules 170 | output_context = { 171 | enabled = local.enabled 172 | namespace = local.namespace 173 | tenant = local.tenant 174 | environment = local.environment 175 | stage = local.stage 176 | name = local.name 177 | delimiter = local.delimiter 178 | attributes = local.attributes 179 | tags = local.tags 180 | additional_tag_map = local.additional_tag_map 181 | label_order = local.label_order 182 | regex_replace_chars = local.regex_replace_chars 183 | id_length_limit = local.id_length_limit 184 | label_key_case = local.label_key_case 185 | label_value_case = local.label_value_case 186 | labels_as_tags = local.labels_as_tags 187 | descriptor_formats = local.descriptor_formats 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = local.enabled ? local.id : "" 3 | description = "Disambiguated ID string restricted to `id_length_limit` characters in total" 4 | } 5 | 6 | output "id_full" { 7 | value = local.enabled ? local.id_full : "" 8 | description = "ID string not restricted in length" 9 | } 10 | 11 | output "enabled" { 12 | value = local.enabled 13 | description = "True if module is enabled, false otherwise" 14 | } 15 | 16 | output "namespace" { 17 | value = local.enabled ? local.namespace : "" 18 | description = "Normalized namespace" 19 | } 20 | 21 | output "tenant" { 22 | value = local.enabled ? local.tenant : "" 23 | description = "Normalized tenant" 24 | } 25 | 26 | output "environment" { 27 | value = local.enabled ? local.environment : "" 28 | description = "Normalized environment" 29 | } 30 | 31 | output "name" { 32 | value = local.enabled ? local.name : "" 33 | description = "Normalized name" 34 | } 35 | 36 | output "stage" { 37 | value = local.enabled ? local.stage : "" 38 | description = "Normalized stage" 39 | } 40 | 41 | output "delimiter" { 42 | value = local.enabled ? local.delimiter : "" 43 | description = "Delimiter between `namespace`, `tenant`, `environment`, `stage`, `name` and `attributes`" 44 | } 45 | 46 | output "attributes" { 47 | value = local.enabled ? local.attributes : [] 48 | description = "List of attributes" 49 | } 50 | 51 | output "tags" { 52 | value = local.enabled ? local.tags : {} 53 | description = "Normalized Tag map" 54 | } 55 | 56 | output "additional_tag_map" { 57 | value = local.additional_tag_map 58 | description = "The merged additional_tag_map" 59 | } 60 | 61 | output "label_order" { 62 | value = local.label_order 63 | description = "The naming order actually used to create the ID" 64 | } 65 | 66 | output "regex_replace_chars" { 67 | value = local.regex_replace_chars 68 | description = "The regex_replace_chars actually used to create the ID" 69 | } 70 | 71 | output "id_length_limit" { 72 | value = local.id_length_limit 73 | description = "The id_length_limit actually used to create the ID, with `0` meaning unlimited" 74 | } 75 | 76 | output "tags_as_list_of_maps" { 77 | value = local.tags_as_list_of_maps 78 | description = <<-EOT 79 | This is a list with one map for each `tag`. Each map contains the tag `key`, 80 | `value`, and contents of `var.additional_tag_map`. Used in the rare cases 81 | where resources need additional configuration information for each tag. 82 | EOT 83 | } 84 | 85 | output "descriptors" { 86 | value = local.descriptors 87 | description = "Map of descriptors as configured by `descriptor_formats`" 88 | } 89 | 90 | output "normalized_context" { 91 | value = local.output_context 92 | description = "Normalized context of this module" 93 | } 94 | 95 | output "context" { 96 | value = local.input 97 | description = <<-EOT 98 | Merged but otherwise unmodified input to this module, to be used as context input to other modules. 99 | Note: this version will have null values as defaults, not the values actually used as defaults. 100 | EOT 101 | } 102 | 103 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | .test-harness 2 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | TEST_HARNESS ?= https://github.com/cloudposse/test-harness.git 2 | TEST_HARNESS_BRANCH ?= master 3 | TEST_HARNESS_PATH = $(realpath .test-harness) 4 | BATS_ARGS ?= --tap 5 | BATS_LOG ?= test.log 6 | 7 | # Define a macro to run the tests 8 | define RUN_TESTS 9 | @echo "Running tests in $(1)" 10 | @cd $(1) && bats $(BATS_ARGS) $(addsuffix .bats,$(addprefix $(TEST_HARNESS_PATH)/test/terraform/,$(TESTS))) 11 | endef 12 | 13 | default: all 14 | 15 | -include Makefile.* 16 | 17 | ## Provision the test-harnesss 18 | .test-harness: 19 | [ -d $@ ] || git clone --depth=1 -b $(TEST_HARNESS_BRANCH) $(TEST_HARNESS) $@ 20 | 21 | ## Initialize the tests 22 | init: .test-harness 23 | 24 | ## Install all dependencies (OS specific) 25 | deps:: 26 | @exit 0 27 | 28 | ## Clean up the test harness 29 | clean: 30 | [ "$(TEST_HARNESS_PATH)" == "/" ] || rm -rf $(TEST_HARNESS_PATH) 31 | 32 | ## Run all tests 33 | all: module examples/complete 34 | 35 | ## Run basic sanity checks against the module itself 36 | module: export TESTS ?= installed lint module-pinning provider-pinning validate terraform-docs input-descriptions output-descriptions 37 | module: deps 38 | $(call RUN_TESTS, ../) 39 | 40 | ## Run tests against example 41 | examples/complete: export TESTS ?= installed lint validate 42 | examples/complete: deps 43 | $(call RUN_TESTS, ../$@) 44 | -------------------------------------------------------------------------------- /test/Makefile.alpine: -------------------------------------------------------------------------------- 1 | ifneq (,$(wildcard /sbin/apk)) 2 | ## Install all dependencies for alpine 3 | deps:: init 4 | @apk add --update terraform-docs@cloudposse json2hcl@cloudposse 5 | endif 6 | -------------------------------------------------------------------------------- /test/src/.gitignore: -------------------------------------------------------------------------------- 1 | .gopath 2 | vendor/ 3 | -------------------------------------------------------------------------------- /test/src/Makefile: -------------------------------------------------------------------------------- 1 | export TERRAFORM_VERSION ?= $(shell curl -s https://checkpoint-api.hashicorp.com/v1/check/terraform | jq -r -M '.current_version' | cut -d. -f1) 2 | 3 | .DEFAULT_GOAL : all 4 | .PHONY: all 5 | 6 | ## Default target 7 | all: test 8 | 9 | .PHONY : init 10 | ## Initialize tests 11 | init: 12 | @exit 0 13 | 14 | .PHONY : test 15 | ## Run tests 16 | test: init 17 | go mod download 18 | go test -v -timeout 10m 19 | 20 | ## Run tests in docker container 21 | docker/test: 22 | docker run --name terratest --rm -it -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN -e GITHUB_TOKEN \ 23 | -e PATH="/usr/local/terraform/$(TERRAFORM_VERSION)/bin:/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ 24 | -v $(CURDIR)/../../:/module/ cloudposse/test-harness:latest -C /module/test/src test 25 | 26 | .PHONY : clean 27 | ## Clean up files 28 | clean: 29 | rm -rf ../../examples/complete/*.tfstate* 30 | -------------------------------------------------------------------------------- /test/src/examples_complete_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | test_structure "github.com/gruntwork-io/terratest/modules/test-structure" 9 | 10 | "github.com/gruntwork-io/terratest/modules/terraform" 11 | "github.com/qdm12/reprint" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func cleanup(t *testing.T, terraformOptions *terraform.Options, tempTestFolder string) { 16 | terraform.Destroy(t, terraformOptions) 17 | os.RemoveAll(tempTestFolder) 18 | } 19 | 20 | type NLContext struct { 21 | AdditionalTagMap map[string]string `json:"additional_tag_map"` 22 | Attributes []string `json:"attributes"` 23 | Delimiter interface{} `json:"delimiter"` 24 | Enabled bool `json:"enabled"` 25 | Environment interface{} `json:"environment"` 26 | LabelOrder []string `json:"label_order"` 27 | Name interface{} `json:"name"` 28 | Namespace interface{} `json:"namespace"` 29 | RegexReplaceChars interface{} `json:"regex_replace_chars"` 30 | Stage interface{} `json:"stage"` 31 | Tags map[string]string `json:"tags"` 32 | Tenant interface{} `json:"tenant"` 33 | } 34 | 35 | // Test the Terraform module in examples/complete using Terratest. 36 | func TestExamplesComplete(t *testing.T) { 37 | t.Parallel() 38 | 39 | rootFolder := "../../" 40 | terraformFolderRelativeToRoot := "examples/complete" 41 | 42 | tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) 43 | 44 | terraformOptions := &terraform.Options{ 45 | // The path to where our Terraform code is located 46 | TerraformDir: tempTestFolder, 47 | Upgrade: true, 48 | } 49 | 50 | // At the end of the test, run `terraform destroy` to clean up any resources that were created 51 | defer terraform.Destroy(t, terraformOptions) 52 | 53 | // This will run `terraform init` and `terraform apply` and fail the test if there are any errors 54 | terraform.InitAndApply(t, terraformOptions) 55 | 56 | compatible := terraform.Output(t, terraformOptions, "compatible") 57 | assert.Equal(t, "true", compatible) 58 | 59 | expectedDescriptorAccountName := "bild-hrh" 60 | expectedDescriptorStack := "hrh-uat-bild" 61 | descriptorAccountName := terraform.Output(t, terraformOptions, "descriptor_account_name") 62 | descriptorStack := terraform.Output(t, terraformOptions, "descriptor_stack") 63 | assert.Equal(t, expectedDescriptorAccountName, descriptorAccountName) 64 | assert.Equal(t, expectedDescriptorStack, descriptorStack) 65 | 66 | chainedDescriptorAccountName := terraform.Output(t, terraformOptions, "chained_descriptor_account_name") 67 | chainedDescriptorStack := terraform.Output(t, terraformOptions, "chained_descriptor_stack") 68 | assert.Equal(t, descriptorAccountName, chainedDescriptorAccountName, "Chained module should output same descriptors") 69 | assert.Equal(t, descriptorStack, chainedDescriptorStack, "Chained module should output same descriptors") 70 | 71 | expectedLabel1Context := NLContext{ 72 | Enabled: true, 73 | Namespace: "CloudPosse", 74 | Tenant: "H.R.H", 75 | Environment: "UAT", 76 | Stage: "build", 77 | Name: "Winston Churchroom", 78 | Attributes: []string{"fire", "water", "earth", "air"}, 79 | Delimiter: nil, 80 | LabelOrder: []string{"name", "tenant", "environment", "stage", "attributes"}, 81 | Tags: map[string]string{ 82 | "City": "Dublin", 83 | "Environment": "Private", 84 | }, 85 | AdditionalTagMap: map[string]string{}, 86 | } 87 | 88 | var expectedLabel1NormalizedContext NLContext 89 | _ = reprint.FromTo(&expectedLabel1Context, &expectedLabel1NormalizedContext) 90 | expectedLabel1NormalizedContext.Namespace = "cloudposse" 91 | expectedLabel1NormalizedContext.Tenant = "hrh" 92 | expectedLabel1NormalizedContext.Environment = "uat" 93 | expectedLabel1NormalizedContext.Name = "winstonchurchroom" 94 | expectedLabel1NormalizedContext.Delimiter = "-" 95 | expectedLabel1NormalizedContext.RegexReplaceChars = "/[^-a-zA-Z0-9]/" 96 | expectedLabel1NormalizedContext.Tags = map[string]string{ 97 | "City": "Dublin", 98 | "Environment": "Private", 99 | "Namespace": "cloudposse", 100 | "Stage": "build", 101 | "Tenant": "hrh", 102 | "Name": "winstonchurchroom-hrh-uat-build-fire-water-earth-air", 103 | "Attributes": "fire-water-earth-air", 104 | } 105 | 106 | var label1NormalizedContext, label1Context NLContext 107 | // Run `terraform output` to get the value of an output variable 108 | label1 := terraform.OutputMap(t, terraformOptions, "label1") 109 | label1Tags := terraform.OutputMap(t, terraformOptions, "label1_tags") 110 | terraform.OutputStruct(t, terraformOptions, "label1_normalized_context", &label1NormalizedContext) 111 | terraform.OutputStruct(t, terraformOptions, "label1_context", &label1Context) 112 | 113 | // Verify we're getting back the outputs we expect 114 | assert.Equal(t, "winstonchurchroom-hrh-uat-build-fire-water-earth-air", label1["id"]) 115 | assert.Equal(t, "winstonchurchroom-hrh-uat-build-fire-water-earth-air", label1Tags["Name"]) 116 | assert.Equal(t, "Dublin", label1Tags["City"]) 117 | assert.Equal(t, "Private", label1Tags["Environment"]) 118 | assert.Equal(t, expectedLabel1NormalizedContext, label1NormalizedContext) 119 | assert.Equal(t, expectedLabel1Context, label1Context) 120 | 121 | label1t1 := terraform.OutputMap(t, terraformOptions, "label1t1") 122 | label1t1Tags := terraform.OutputMap(t, terraformOptions, "label1t1_tags") 123 | assert.Equal(t, "winstonchurchroom-hrh-uat-6403d8", label1t1["id"], 124 | "Extra hash character should be added when trailing delimiter is removed") 125 | assert.Equal(t, label1["id"], label1t1["id_full"], "id_full should not be truncated") 126 | assert.Equal(t, label1t1["id"], label1t1Tags["Name"], "Name tag should match ID") 127 | 128 | label1t2 := terraform.OutputMap(t, terraformOptions, "label1t2") 129 | label1t2Tags := terraform.OutputMap(t, terraformOptions, "label1t2_tags") 130 | assert.Equal(t, "winstonchurchroom-hrh-uat-b-6403d", label1t2["id"]) 131 | assert.Equal(t, label1t2["id"], label1t2Tags["Name"], "Name tag should match ID") 132 | 133 | // Run `terraform output` to get the value of an output variable 134 | label2 := terraform.OutputMap(t, terraformOptions, "label2") 135 | label2Tags := terraform.OutputMap(t, terraformOptions, "label2_tags") 136 | 137 | // Verify we're getting back the outputs we expect 138 | assert.Equal(t, "charlie+uat+test+fire+water+earth+air", label2["id"]) 139 | assert.Equal(t, "charlie+uat+test+fire+water+earth+air", label2Tags["Name"]) 140 | assert.Equal(t, "London", label2Tags["City"]) 141 | assert.Equal(t, "Public", label2Tags["Environment"]) 142 | 143 | var expectedLabel3cContext, label3cContext NLContext 144 | _ = reprint.FromTo(&expectedLabel1Context, &expectedLabel3cContext) 145 | expectedLabel3cContext.Name = "Starfish" 146 | expectedLabel3cContext.Stage = "release" 147 | expectedLabel3cContext.Delimiter = "." 148 | expectedLabel3cContext.RegexReplaceChars = "/[^-a-zA-Z0-9.]/" 149 | expectedLabel3cContext.Tags["Eat"] = "Carrot" 150 | expectedLabel3cContext.Tags["Animal"] = "Rabbit" 151 | 152 | // Run `terraform output` to get the value of an output variable 153 | label3c := terraform.OutputMap(t, terraformOptions, "label3c") 154 | label3cTags := terraform.OutputMap(t, terraformOptions, "label3c_tags") 155 | terraform.OutputStruct(t, terraformOptions, "label3c_context", &label3cContext) 156 | 157 | // Verify we're getting back the outputs we expect 158 | assert.Equal(t, "starfish.h.r.h.uat.release.fire.water.earth.air", label3c["id"]) 159 | assert.Equal(t, "starfish.h.r.h.uat.release.fire.water.earth.air", label3cTags["Name"]) 160 | assert.Equal(t, expectedLabel3cContext, label3cContext) 161 | 162 | var expectedLabel3nContext, label3nContext NLContext 163 | _ = reprint.FromTo(&expectedLabel1NormalizedContext, &expectedLabel3nContext) 164 | expectedLabel3nContext.Name = "Starfish" 165 | expectedLabel3nContext.Stage = "release" 166 | expectedLabel3nContext.Delimiter = "." 167 | expectedLabel3nContext.RegexReplaceChars = "/[^-a-zA-Z0-9.]/" 168 | expectedLabel3nContext.Tags["Eat"] = "Carrot" 169 | expectedLabel3nContext.Tags["Animal"] = "Rabbit" 170 | 171 | // Run `terraform output` to get the value of an output variable 172 | label3n := terraform.OutputMap(t, terraformOptions, "label3n") 173 | label3nTags := terraform.OutputMap(t, terraformOptions, "label3n_tags") 174 | terraform.OutputStruct(t, terraformOptions, "label3n_context", &label3nContext) 175 | 176 | // Verify we're getting back the outputs we expect 177 | // The tenant from normalized label1 should be "hrh" not "h.r.h." 178 | assert.Equal(t, "starfish.hrh.uat.release.fire.water.earth.air", label3n["id"]) 179 | assert.Equal(t, label1Tags["Name"], label3nTags["Name"], 180 | "Tag from label1 normalized context should overwrite label3n generated tag") 181 | assert.Equal(t, expectedLabel3nContext, label3nContext) 182 | 183 | // Run `terraform output` to get the value of an output variable 184 | label4 := terraform.OutputMap(t, terraformOptions, "label4") 185 | label4Tags := terraform.OutputMap(t, terraformOptions, "label4_tags") 186 | 187 | // Verify we're getting back the outputs we expect 188 | assert.Equal(t, "cloudposse-uat-big-fat-honking-cluster", label4["id"]) 189 | assert.Equal(t, "cloudposse-uat-big-fat-honking-cluster", label4Tags["Name"]) 190 | 191 | // Run `terraform output` to get the value of an output variable 192 | label5 := terraform.OutputMap(t, terraformOptions, "label5") 193 | 194 | // Verify we're getting back the outputs we expect 195 | assert.Equal(t, "", label5["id"]) 196 | 197 | label6f := terraform.OutputMap(t, terraformOptions, "label6f") 198 | label6fTags := terraform.OutputMap(t, terraformOptions, "label6f_tags") 199 | // Test of setting var.label_key_case = "lower", var.label_value_case = "upper" 200 | assert.Equal(t, "CP~UW2~PRD~NULL-LABEL", label6f["id_full"]) 201 | assert.Equal(t, label6f["id_full"], label6f["id"], "id should not be truncated") 202 | assert.Equal(t, label6f["id"], label6fTags["name"], "Name tag should match ID") 203 | 204 | label6t := terraform.OutputMap(t, terraformOptions, "label6t") 205 | label6tTags := terraform.OutputMap(t, terraformOptions, "label6t_tags") 206 | assert.Equal(t, "CPUW2PRDNULL-LABEL", label6t["id_full"]) 207 | assert.NotEqual(t, label6t["id_full"], label6t["id"], "id should be truncated") 208 | assert.Equal(t, label6t["id"], label6tTags["name"], "Name tag should match ID") 209 | assert.Equal(t, label6t["id_length_limit"], fmt.Sprintf("%d", len(label6t["id"])), 210 | "Truncated ID length should equal length limit") 211 | 212 | label7 := terraform.OutputMap(t, terraformOptions, "label7") 213 | assert.Equal(t, "eg-demo-blue-cluster-nodegroup", label7["id"], "var.attributes should be appended after context.attributes") 214 | 215 | // Verify that apply with `label_key_case=title`, `label_value_case=lower`, `delimiter=""` returns expected value of id, context id 216 | label8dndID := terraform.Output(t, terraformOptions, "label8dnd_id") 217 | label8dndContextID := terraform.Output(t, terraformOptions, "label8dnd_context_id") 218 | assert.Equal(t, "egdemobluecluster", label8dndID) 219 | assert.Equal(t, label8dndID, label8dndContextID, "ID and context ID should be equal") 220 | 221 | // Verify that apply with `label_key_case=title`, `label_value_case=lower`, `delimiter="x"` returns expected value of id, context id 222 | label8dcdID := terraform.Output(t, terraformOptions, "label8dcd_id") 223 | label8dcdContextID := terraform.Output(t, terraformOptions, "label8dcd_context_id") 224 | assert.Equal(t, "egxdemoxbluexcluster", label8dcdID) 225 | assert.Equal(t, label8dcdID, label8dcdContextID, "ID and context ID should be equal") 226 | 227 | // Verify that apply with `label_key_case=title` and `label_value_case=lower` returns expected values of id, tags, context tags 228 | label8dExpectedTags := map[string]string{ 229 | "Attributes": "cluster", 230 | "Environment": "demo", 231 | "Name": "eg-demo-blue-cluster", 232 | // Suppressed by labels_as_tags: "Namespace": "eg", 233 | "kubernetes.io/cluster/": "shared", 234 | } 235 | 236 | label8dID := terraform.Output(t, terraformOptions, "label8d_id") 237 | label8dContextID := terraform.Output(t, terraformOptions, "label8d_context_id") 238 | label8dChained := terraform.Output(t, terraformOptions, "label8d_chained_context_labels_as_tags") 239 | assert.Equal(t, "eg-demo-blue-cluster", label8dID) 240 | assert.Equal(t, label8dID, label8dContextID, "ID and context ID should be equal") 241 | assert.Equal(t, "attributes-environment-name-stage", label8dChained) 242 | 243 | label8dTags := terraform.OutputMap(t, terraformOptions, "label8d_tags") 244 | label8dContextTags := terraform.OutputMap(t, terraformOptions, "label8d_context_tags") 245 | 246 | assert.Exactly(t, label8dExpectedTags, label8dTags, "generated tags are different from expected") 247 | assert.Exactly(t, label8dTags, label8dContextTags, "tags and context tags should be equal") 248 | 249 | // Verify that apply with `label_key_case=lower` and `label_value_case=lower` returns expected values of id, tags, context tags 250 | label8lExpectedTags := map[string]string{ 251 | "attributes": "cluster", 252 | "environment": "demo", 253 | "name": "eg-demo-blue-cluster", 254 | "namespace": "eg", 255 | "kubernetes.io/cluster/": "shared", 256 | "upperTEST": "testUPPER", 257 | } 258 | 259 | label8lID := terraform.Output(t, terraformOptions, "label8l_id") 260 | label8lContextID := terraform.Output(t, terraformOptions, "label8l_context_id") 261 | assert.Equal(t, "eg-demo-blue-cluster", label8lID) 262 | assert.Equal(t, label8lID, label8lContextID, "ID and context ID should be equal") 263 | 264 | label8lTags := terraform.OutputMap(t, terraformOptions, "label8l_tags") 265 | label8lContextTags := terraform.OutputMap(t, terraformOptions, "label8l_context_tags") 266 | 267 | assert.Exactly(t, label8lExpectedTags, label8lTags, "generated tags are different from expected") 268 | assert.Exactly(t, label8lTags, label8lContextTags, "tags and context tags should be equal") 269 | 270 | // Verify that apply with `label_key_case=title` and `label_value_case=title` returns expected values of id, tags, context tags 271 | label8tExpectedTags := map[string]string{ 272 | "Attributes": "Eks-Cluster", 273 | "Environment": "Demo", 274 | "Name": "Eg-Demo-Blue-Eks-Cluster", 275 | "Namespace": "Eg", 276 | "kubernetes.io/cluster/": "shared", 277 | } 278 | 279 | label8tID := terraform.Output(t, terraformOptions, "label8t_id") 280 | label8tContextID := terraform.Output(t, terraformOptions, "label8t_context_id") 281 | assert.Equal(t, "Eg-Demo-Blue-Eks-Cluster", label8tID) 282 | assert.Equal(t, label8tID, label8tContextID, "ID and context ID should be equal") 283 | 284 | label8tTags := terraform.OutputMap(t, terraformOptions, "label8t_tags") 285 | label8tContextTags := terraform.OutputMap(t, terraformOptions, "label8t_context_tags") 286 | 287 | assert.Exactly(t, label8tExpectedTags, label8tTags, "generated tags are different from expected") 288 | assert.Exactly(t, label8tTags, label8tContextTags, "tags and context tags should be equal") 289 | 290 | // Verify that apply with `label_key_case=upper` and `label_value_case=upper` returns expected values of id, tags, context tags 291 | label8uExpectedTags := map[string]string{ 292 | "ATTRIBUTES": "CLUSTER", 293 | "ENVIRONMENT": "DEMO", 294 | "NAME": "EG-DEMO-BLUE-CLUSTER", 295 | "NAMESPACE": "EG", 296 | "kubernetes.io/cluster/": "shared", 297 | } 298 | 299 | label8uID := terraform.Output(t, terraformOptions, "label8u_id") 300 | label8uContextID := terraform.Output(t, terraformOptions, "label8u_context_id") 301 | assert.Equal(t, "EG-DEMO-BLUE-CLUSTER", label8uID) 302 | assert.Equal(t, label8uID, label8uContextID, "ID and context ID should be equal") 303 | 304 | label8uTags := terraform.OutputMap(t, terraformOptions, "label8u_tags") 305 | label8uContextTags := terraform.OutputMap(t, terraformOptions, "label8u_context_tags") 306 | 307 | assert.Exactly(t, label8uExpectedTags, label8uTags, "generated tags are different from expected") 308 | assert.Exactly(t, label8uTags, label8uContextTags, "tags and context tags should be equal") 309 | 310 | // Verify that apply with `label_key_case=title` and `label_value_case=none` returns expected values of id, tags, context tags 311 | label8nExpectedTags := map[string]string{ 312 | "Attributes": "eks-ClusteR", 313 | "Environment": "demo", 314 | "Name": "EG-demo-blue-eks-ClusteR", 315 | "Namespace": "EG", 316 | "kubernetes.io/cluster/": "shared", 317 | } 318 | 319 | label8nID := terraform.Output(t, terraformOptions, "label8n_id") 320 | label8nContextID := terraform.Output(t, terraformOptions, "label8n_context_id") 321 | assert.Equal(t, "EG-demo-blue-eks-ClusteR", label8nID) 322 | assert.Equal(t, label8nID, label8nContextID, "ID and context ID should be equal") 323 | 324 | label8nTags := terraform.OutputMap(t, terraformOptions, "label8n_tags") 325 | label8nContextTags := terraform.OutputMap(t, terraformOptions, "label8n_context_tags") 326 | 327 | assert.Exactly(t, label8nExpectedTags, label8nTags, "generated tags are different from expected") 328 | assert.Exactly(t, label8nTags, label8nContextTags, "tags and context tags should be equal") 329 | } 330 | -------------------------------------------------------------------------------- /test/src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudposse/terraform-null-label 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/gruntwork-io/terratest v0.39.0 9 | github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 10 | github.com/stretchr/testify v1.7.0 11 | ) 12 | 13 | require ( 14 | cloud.google.com/go v0.83.0 // indirect 15 | cloud.google.com/go/storage v1.10.0 // indirect 16 | github.com/agext/levenshtein v1.2.3 // indirect 17 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 18 | github.com/aws/aws-sdk-go v1.40.56 // indirect 19 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 20 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect 21 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect 24 | github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect 25 | github.com/go-logr/logr v0.2.0 // indirect 26 | github.com/go-sql-driver/mysql v1.4.1 // indirect 27 | github.com/gogo/protobuf v1.3.2 // indirect 28 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 29 | github.com/golang/protobuf v1.5.2 // indirect 30 | github.com/golang/snappy v0.0.3 // indirect 31 | github.com/google/gofuzz v1.1.0 // indirect 32 | github.com/google/uuid v1.2.0 // indirect 33 | github.com/googleapis/gax-go/v2 v2.0.5 // indirect 34 | github.com/googleapis/gnostic v0.4.1 // indirect 35 | github.com/gruntwork-io/go-commons v0.8.0 // indirect 36 | github.com/hashicorp/errwrap v1.0.0 // indirect 37 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 38 | github.com/hashicorp/go-getter v1.5.9 // indirect 39 | github.com/hashicorp/go-multierror v1.1.0 // indirect 40 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 41 | github.com/hashicorp/go-version v1.3.0 // indirect 42 | github.com/hashicorp/hcl/v2 v2.9.1 // indirect 43 | github.com/hashicorp/terraform-json v0.13.0 // indirect 44 | github.com/imdario/mergo v0.3.11 // indirect 45 | github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a // indirect 46 | github.com/jmespath/go-jmespath v0.4.0 // indirect 47 | github.com/json-iterator/go v1.1.11 // indirect 48 | github.com/jstemmer/go-junit-report v0.9.1 // indirect 49 | github.com/klauspost/compress v1.13.0 // indirect 50 | github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect 51 | github.com/mitchellh/go-homedir v1.1.0 // indirect 52 | github.com/mitchellh/go-testing-interface v1.0.0 // indirect 53 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 55 | github.com/modern-go/reflect2 v1.0.1 // indirect 56 | github.com/pmezard/go-difflib v1.0.0 // indirect 57 | github.com/pquerna/otp v1.2.0 // indirect 58 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 59 | github.com/spf13/pflag v1.0.5 // indirect 60 | github.com/tmccombs/hcl2json v0.3.3 // indirect 61 | github.com/ulikunitz/xz v0.5.8 // indirect 62 | github.com/urfave/cli v1.22.2 // indirect 63 | github.com/zclconf/go-cty v1.9.1 // indirect 64 | go.opencensus.io v0.23.0 // indirect 65 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect 66 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect 67 | golang.org/x/mod v0.4.2 // indirect 68 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect 69 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect 70 | golang.org/x/sys v0.0.0-20210603125802-9665404d3644 // indirect 71 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect 72 | golang.org/x/text v0.3.6 // indirect 73 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect 74 | golang.org/x/tools v0.1.2 // indirect 75 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 76 | google.golang.org/api v0.47.0 // indirect 77 | google.golang.org/appengine v1.6.7 // indirect 78 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect 79 | google.golang.org/grpc v1.38.0 // indirect 80 | google.golang.org/protobuf v1.26.0 // indirect 81 | gopkg.in/inf.v0 v0.9.1 // indirect 82 | gopkg.in/yaml.v2 v2.4.0 // indirect 83 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 84 | k8s.io/api v0.20.6 // indirect 85 | k8s.io/apimachinery v0.20.6 // indirect 86 | k8s.io/client-go v0.20.6 // indirect 87 | k8s.io/klog/v2 v2.4.0 // indirect 88 | k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect 89 | sigs.k8s.io/structured-merge-diff/v4 v4.0.3 // indirect 90 | sigs.k8s.io/yaml v1.2.0 // indirect 91 | ) 92 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "context" { 2 | type = any 3 | default = { 4 | enabled = true 5 | namespace = null 6 | tenant = null 7 | environment = null 8 | stage = null 9 | name = null 10 | delimiter = null 11 | attributes = [] 12 | tags = {} 13 | additional_tag_map = {} 14 | regex_replace_chars = null 15 | label_order = [] 16 | id_length_limit = null 17 | label_key_case = null 18 | label_value_case = null 19 | descriptor_formats = {} 20 | # Note: we have to use [] instead of null for unset lists due to 21 | # https://github.com/hashicorp/terraform/issues/28137 22 | # which was not fixed until Terraform 1.0.0, 23 | # but we want the default to be all the labels in `label_order` 24 | # and we want users to be able to prevent all tag generation 25 | # by setting `labels_as_tags` to `[]`, so we need 26 | # a different sentinel to indicate "default" 27 | labels_as_tags = ["unset"] 28 | } 29 | description = <<-EOT 30 | Single object for setting entire context at once. 31 | See description of individual variables for details. 32 | Leave string and numeric variables as `null` to use default value. 33 | Individual variable settings (non-null) override settings in context object, 34 | except for attributes, tags, and additional_tag_map, which are merged. 35 | EOT 36 | 37 | validation { 38 | condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) 39 | error_message = "Allowed values: `lower`, `title`, `upper`." 40 | } 41 | 42 | validation { 43 | condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) 44 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 45 | } 46 | } 47 | 48 | variable "enabled" { 49 | type = bool 50 | default = null 51 | description = "Set to false to prevent the module from creating any resources" 52 | } 53 | 54 | variable "namespace" { 55 | type = string 56 | default = null 57 | description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" 58 | } 59 | 60 | variable "tenant" { 61 | type = string 62 | default = null 63 | description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" 64 | } 65 | 66 | variable "environment" { 67 | type = string 68 | default = null 69 | description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" 70 | } 71 | 72 | variable "stage" { 73 | type = string 74 | default = null 75 | description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" 76 | } 77 | 78 | variable "name" { 79 | type = string 80 | default = null 81 | description = <<-EOT 82 | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. 83 | This is the only ID element not also included as a `tag`. 84 | The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. 85 | EOT 86 | } 87 | 88 | variable "delimiter" { 89 | type = string 90 | default = null 91 | description = <<-EOT 92 | Delimiter to be used between ID elements. 93 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 94 | EOT 95 | } 96 | 97 | variable "attributes" { 98 | type = list(string) 99 | default = [] 100 | description = <<-EOT 101 | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, 102 | in the order they appear in the list. New attributes are appended to the 103 | end of the list. The elements of the list are joined by the `delimiter` 104 | and treated as a single ID element. 105 | EOT 106 | } 107 | 108 | variable "labels_as_tags" { 109 | type = set(string) 110 | default = ["default"] 111 | description = <<-EOT 112 | Set of labels (ID elements) to include as tags in the `tags` output. 113 | Default is to include all labels. 114 | Tags with empty values will not be included in the `tags` output. 115 | Set to `[]` to suppress all generated tags. 116 | **Notes:** 117 | The value of the `name` tag, if included, will be the `id`, not the `name`. 118 | Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be 119 | changed in later chained modules. Attempts to change it will be silently ignored. 120 | EOT 121 | } 122 | 123 | variable "tags" { 124 | type = map(string) 125 | default = {} 126 | description = <<-EOT 127 | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). 128 | Neither the tag keys nor the tag values will be modified by this module. 129 | EOT 130 | } 131 | 132 | variable "additional_tag_map" { 133 | type = map(string) 134 | default = {} 135 | description = <<-EOT 136 | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. 137 | This is for some rare cases where resources want additional configuration of tags 138 | and therefore take a list of maps with tag key, value, and additional configuration. 139 | EOT 140 | } 141 | 142 | variable "label_order" { 143 | type = list(string) 144 | default = null 145 | description = <<-EOT 146 | The order in which the labels (ID elements) appear in the `id`. 147 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 148 | You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. 149 | EOT 150 | } 151 | 152 | variable "regex_replace_chars" { 153 | type = string 154 | default = null 155 | description = <<-EOT 156 | Terraform regular expression (regex) string. 157 | Characters matching the regex will be removed from the ID elements. 158 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 159 | EOT 160 | } 161 | 162 | variable "id_length_limit" { 163 | type = number 164 | default = null 165 | description = <<-EOT 166 | Limit `id` to this many characters (minimum 6). 167 | Set to `0` for unlimited length. 168 | Set to `null` for keep the existing setting, which defaults to `0`. 169 | Does not affect `id_full`. 170 | EOT 171 | validation { 172 | condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 173 | error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." 174 | } 175 | } 176 | 177 | variable "label_key_case" { 178 | type = string 179 | default = null 180 | description = <<-EOT 181 | Controls the letter case of the `tags` keys (label names) for tags generated by this module. 182 | Does not affect keys of tags passed in via the `tags` input. 183 | Possible values: `lower`, `title`, `upper`. 184 | Default value: `title`. 185 | EOT 186 | 187 | validation { 188 | condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) 189 | error_message = "Allowed values: `lower`, `title`, `upper`." 190 | } 191 | } 192 | 193 | variable "label_value_case" { 194 | type = string 195 | default = null 196 | description = <<-EOT 197 | Controls the letter case of ID elements (labels) as included in `id`, 198 | set as tag values, and output by this module individually. 199 | Does not affect values of tags passed in via the `tags` input. 200 | Possible values: `lower`, `title`, `upper` and `none` (no transformation). 201 | Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. 202 | Default value: `lower`. 203 | EOT 204 | 205 | validation { 206 | condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) 207 | error_message = "Allowed values: `lower`, `title`, `upper`, `none`." 208 | } 209 | } 210 | 211 | variable "descriptor_formats" { 212 | type = any 213 | default = {} 214 | description = <<-EOT 215 | Describe additional descriptors to be output in the `descriptors` output map. 216 | Map of maps. Keys are names of descriptors. Values are maps of the form 217 | `{ 218 | format = string 219 | labels = list(string) 220 | }` 221 | (Type is `any` so the map values can later be enhanced to provide additional options.) 222 | `format` is a Terraform format string to be passed to the `format()` function. 223 | `labels` is a list of labels, in order, to pass to `format()` function. 224 | Label values will be normalized before being passed to `format()` so they will be 225 | identical to how they appear in `id`. 226 | Default is `{}` (`descriptors` output will be empty). 227 | EOT 228 | } 229 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.13.0" 3 | } 4 | --------------------------------------------------------------------------------