├── .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 | [](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 | [](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 | 
5 |
6 |
7 |



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 | >
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 | >
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 | >
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 |
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 |
963 |
964 |
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 |
--------------------------------------------------------------------------------