From 6edc9b2bfaf54aaa87fa20ede34db2e757360a63 Mon Sep 17 00:00:00 2001 From: Erik Osterman Date: Tue, 24 Jul 2018 14:02:54 -0700 Subject: [PATCH] Add context (#25) * Add support for context variable * Initial prototype of concept * format code * Better example * Much magic applied * More magic applied * fixed duplication of strings after passing context * fixed duplication of strings after passing context * Got the merge order for the tags correct * Added some comments * Removed the lower() function from the delimiter string normalisation * More comments * Changed to use replace() for removing whitespace in values * Joined the attributes together * Joined the attributes together * Handles empty values from variables * Updated Readme * wrong delimiter in tags, delimiter output added (#31) * Updated README to include the context input and output, added environment as an optional variable. Swapped out null_resource for null_data_source, which reduces some of the output noise but keeps the funtionality the same. --- .gitignore | 19 ++++--- README.md | 107 +++++++++++++++++++++++++++++++++-- README.yaml | 98 +++++++++++++++++++++++++++++++- docs/terraform.md | 10 +++- examples/complete/main.tf | 42 +++++++++++--- examples/complete/outputs.tf | 54 ++++++++++++++---- main.tf | 70 ++++++++++++++++------- outputs.tf | 20 +++++-- variables.tf | 16 +++++- 9 files changed, 375 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index cf417ec..10db001 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,15 @@ -# Compiled files +# Local .terraform directories +**/.terraform/* + +# .tfstate files *.tfstate -*.tfstate.backup +*.tfstate.* + +# .tfvars files +*.tfvars -.terraform -.idea -*.iml +**/.idea +**/*.iml -.build-harness -build-harness +**/.build-harness +**/build-harness diff --git a/README.md b/README.md index 5e48a10..3e44c0f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ Terraform module designed to generate consistent label names and tags for resources. Use `terraform-null-label` to implement a strict naming convention. -A label follows the following convention: `{namespace}-{stage}-{name}-{attributes}`. The delimiter (e.g. `-`) is interchangeable. +A label follows the following convention: `{namespace}-{environment}-{stage}-{name}-{attributes}`. The delimiter (e.g. `-`) is interchangeable. +The label items are all optional. So if you perfer the term `stage` to `environment` you can exclude environment and the label `id` will look like `{namespace}-{stage}-{name}-{attributes}`. +If attributes are excluded but `stage` and `environment` are included, `id` will look like `{namespace}-{environment}-{stage}-{name}` It's recommended to use one `terraform-null-label` module for every unique resource of a given resource type. For example, if you have 10 instances, there should be 10 different labels. @@ -226,6 +228,97 @@ resource "aws_autoscaling_group" "default" { tags = ["${module.label.tags_as_list_of_maps}"] } ``` +### Advanced Example 3 as per the [complete example](./examples/complete) +This example shows how you can pass the `context` output of one label module to the next label_module. +Allowing you to create one label that has the base set of values, and then creating every extra label +as a derivative of that. + +```hcl +module "label1" { + source = "../../" + namespace = "CloudPosse" + environment = "UAT" + stage = "build" + name = "Winston Churchroom" + attributes = ["fire", "water", "earth", "air"] + + tags = { + "City" = "Dublin" + "Environment" = "Private" + } +} + +module "label2" { + source = "../../" + context = "${module.label1.context}" + name = "Charlie" + stage = "test" + + tags = { + "City" = "London" + "Environment" = "Public" + } +} + +module "label3" { + source = "../../" + name = "Starfish" + stage = "release" + + tags = { + "Eat" = "Carrot" + "Animal" = "Rabbit" + } +} +``` + +This creates label outputs like this. +```hcl +Outputs: +label1 = { + attributes = fire-water-earth-air + id = cloudposse-uat-build-winstonchurchroom-fire-water-earth-air + name = winstonchurchroom + namespace = cloudposse + stage = build +} +label1_tags = { + City = Dublin + Environment = Private + Name = cloudposse-uat-build-winstonchurchroom-fire-water-earth-air + Namespace = cloudposse + Stage = build +} +label2 = { + attributes = fire-water-earth-air + id = cloudposse-uat-build-charlie-fire-water-earth-air + name = charlie + namespace = cloudposse + stage = build +} +label2_tags = { + City = London + Environment = Public + Name = cloudposse-uat-build-charlie-fire-water-earth-air + Namespace = cloudposse + Stage = build +} +label3 = { + attributes = + id = release-starfish + name = starfish + namespace = + stage = release +} +label3_tags = { + Animal = Rabbit + Eat = Carrot + Environment = + Name = release-starfish + Namespace = + Stage = release +} +``` @@ -248,11 +341,13 @@ Available targets: |------|-------------|:----:|:-----:|:-----:| | additional_tag_map | Additional tags for appending to each tag map. | map | `` | no | | attributes | Additional attributes (e.g. `policy` or `role`) | list | `` | no | +| context | Default context to use for passing state between label invocations | map | `` | no | | delimiter | Delimiter to be used between `name`, `namespace`, `stage`, etc. | string | `-` | no | | enabled | Set to false to prevent the module from creating any resources | string | `true` | no | -| name | Solution name, e.g. 'app' or 'jenkins' | string | - | yes | -| namespace | Namespace, which could be your organization name, e.g. 'cp' or 'cloudposse' | string | - | yes | -| stage | Stage, e.g. 'prod', 'staging', 'dev', or 'test' | string | - | yes | +| environment | Environment, e.g. 'prod', 'staging', 'dev', 'pre-prod', 'UAT' | string | `` | no | +| name | Solution name, e.g. 'app' or 'jenkins' | string | `` | no | +| namespace | Namespace, which could be your organization name, e.g. 'cp' or 'cloudposse' | string | `` | no | +| stage | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | string | `` | no | | tags | Additional tags (e.g. `map('BusinessUnit','XYZ')` | map | `` | no | ## Outputs @@ -260,6 +355,8 @@ Available targets: | Name | Description | |------|-------------| | attributes | Normalized attributes | +| context | Context of this module to pass between other modules | +| delimiter | Delimiter used in label ID | | id | Disambiguated ID | | name | Normalized name | | namespace | Normalized namespace | @@ -274,7 +371,7 @@ Available targets: Check out these related projects. -- [terraform-terraform-label](https://github.com/cloudposse/terraform-terraform-label) - Terraform Module to define a consistent naming convention by (namespace, stage, name, [attributes]) +- [terraform-terraform-label](https://github.com/cloudposse/terraform-terraform-label) - Terraform Module to define a consistent naming convention by (namespace, environment, stage, name, [attributes]) diff --git a/README.yaml b/README.yaml index a5585f9..5c7c0e6 100644 --- a/README.yaml +++ b/README.yaml @@ -30,14 +30,16 @@ badges: related: - name: "terraform-terraform-label" - description: "Terraform Module to define a consistent naming convention by (namespace, stage, name, [attributes])" + description: "Terraform Module to define a consistent naming convention by (namespace, environment, stage, name, [attributes])" url: "https://github.com/cloudposse/terraform-terraform-label" # Short description of this project description: |- Terraform module designed to generate consistent label names and tags for resources. Use `terraform-null-label` to implement a strict naming convention. - A label follows the following convention: `{namespace}-{stage}-{name}-{attributes}`. The delimiter (e.g. `-`) is interchangeable. + A label follows the following convention: `{namespace}-{environment}-{stage}-{name}-{attributes}`. The delimiter (e.g. `-`) is interchangeable. + The label items are all optional. So if you perfer the term `stage` to `environment` you can exclude environment and the label `id` will look like `{namespace}-{stage}-{name}-{attributes}`. + If attributes are excluded but `stage` and `environment` are included, `id` will look like `{namespace}-{environment}-{stage}-{name}` It's recommended to use one `terraform-null-label` module for every unique resource of a given resource type. For example, if you have 10 instances, there should be 10 different labels. @@ -239,6 +241,98 @@ usage: |- tags = ["${module.label.tags_as_list_of_maps}"] } ``` + ### Advanced Example 3 as per the [complete example](./examples/complete) + This example shows how you can pass the `context` output of one label module to the next label_module. + Allowing you to create one label that has the base set of values, and then creating every extra label + as a derivative of that. + + ```hcl + module "label1" { + source = "../../" + namespace = "CloudPosse" + environment = "UAT" + stage = "build" + name = "Winston Churchroom" + attributes = ["fire", "water", "earth", "air"] + + tags = { + "City" = "Dublin" + "Environment" = "Private" + } + } + + module "label2" { + source = "../../" + context = "${module.label1.context}" + name = "Charlie" + stage = "test" + + tags = { + "City" = "London" + "Environment" = "Public" + } + } + + module "label3" { + source = "../../" + name = "Starfish" + stage = "release" + + tags = { + "Eat" = "Carrot" + "Animal" = "Rabbit" + } + } + ``` + + This creates label outputs like this. + ```hcl + Outputs: + label1 = { + attributes = fire-water-earth-air + id = cloudposse-uat-build-winstonchurchroom-fire-water-earth-air + name = winstonchurchroom + namespace = cloudposse + stage = build + } + label1_tags = { + City = Dublin + Environment = Private + Name = cloudposse-uat-build-winstonchurchroom-fire-water-earth-air + Namespace = cloudposse + Stage = build + } + label2 = { + attributes = fire-water-earth-air + id = cloudposse-uat-build-charlie-fire-water-earth-air + name = charlie + namespace = cloudposse + stage = build + } + label2_tags = { + City = London + Environment = Public + Name = cloudposse-uat-build-charlie-fire-water-earth-air + Namespace = cloudposse + Stage = build + } + label3 = { + attributes = + id = release-starfish + name = starfish + namespace = + stage = release + } + label3_tags = { + Animal = Rabbit + Eat = Carrot + Environment = + Name = release-starfish + Namespace = + Stage = release + } + ``` + include: - "docs/targets.md" diff --git a/docs/terraform.md b/docs/terraform.md index 4b43a92..4ac4139 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -5,11 +5,13 @@ |------|-------------|:----:|:-----:|:-----:| | additional_tag_map | Additional tags for appending to each tag map. | map | `` | no | | attributes | Additional attributes (e.g. `policy` or `role`) | list | `` | no | +| context | Default context to use for passing state between label invocations | map | `` | no | | delimiter | Delimiter to be used between `name`, `namespace`, `stage`, etc. | string | `-` | no | | enabled | Set to false to prevent the module from creating any resources | string | `true` | no | -| name | Solution name, e.g. 'app' or 'jenkins' | string | - | yes | -| namespace | Namespace, which could be your organization name, e.g. 'cp' or 'cloudposse' | string | - | yes | -| stage | Stage, e.g. 'prod', 'staging', 'dev', or 'test' | string | - | yes | +| environment | Environment, e.g. 'prod', 'staging', 'dev', 'pre-prod', 'UAT' | string | `` | no | +| name | Solution name, e.g. 'app' or 'jenkins' | string | `` | no | +| namespace | Namespace, which could be your organization name, e.g. 'cp' or 'cloudposse' | string | `` | no | +| stage | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | string | `` | no | | tags | Additional tags (e.g. `map('BusinessUnit','XYZ')` | map | `` | no | ## Outputs @@ -17,6 +19,8 @@ | Name | Description | |------|-------------| | attributes | Normalized attributes | +| context | Context of this module to pass between other modules | +| delimiter | Delimiter used in label ID | | id | Disambiguated ID | | name | Normalized name | | namespace | Normalized namespace | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 0d96945..9112d19 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -1,8 +1,36 @@ -module "label" { - source = "../../" - namespace = "Namespace" - stage = "Stage" - name = "Name" - attributes = ["1", "2", "3", ""] - tags = "${map("Key", "Value")}" +module "label1" { + source = "../../" + namespace = "CloudPosse" + environment = "UAT" + stage = "build" + name = "Winston Churchroom" + attributes = ["fire", "water", "earth", "air"] + + tags = { + "City" = "Dublin" + "Environment" = "Private" + } +} + +module "label2" { + source = "../../" + context = "${module.label1.context}" + name = "Charlie" + stage = "test" + + tags = { + "City" = "London" + "Environment" = "Public" + } +} + +module "label3" { + source = "../../" + name = "Starfish" + stage = "release" + + tags = { + "Eat" = "Carrot" + "Animal" = "Rabbit" + } } diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index af291b6..d89493a 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -1,23 +1,53 @@ -output "id" { - value = "${module.label.id}" +output "label1" { + value = { + id = "${module.label1.id}" + name = "${module.label1.name}" + namespace = "${module.label1.namespace}" + stage = "${module.label1.stage}" + attributes = "${module.label1.attributes}" + } } -output "name" { - value = "${module.label.name}" +output "label2" { + value = { + id = "${module.label2.id}" + name = "${module.label2.name}" + namespace = "${module.label2.namespace}" + stage = "${module.label2.stage}" + attributes = "${module.label2.attributes}" + } } -output "namespace" { - value = "${module.label.namespace}" +output "label3" { + value = { + id = "${module.label3.id}" + name = "${module.label3.name}" + namespace = "${module.label3.namespace}" + stage = "${module.label3.stage}" + attributes = "${module.label3.attributes}" + } } -output "stage" { - value = "${module.label.stage}" +output "label1_tags" { + value = "${module.label1.tags}" } -output "attributes" { - value = "${module.label.attributes}" +output "label1_context" { + value = "${module.label1.context}" } -output "tags" { - value = "${module.label.tags}" +output "label2_tags" { + value = "${module.label2.tags}" +} + +output "label2_context" { + value = "${module.label2.context}" +} + +output "label3_tags" { + value = "${module.label3.tags}" +} + +output "label3_context" { + value = "${module.label3.context}" } diff --git a/main.tf b/main.tf index 542c8fb..5768274 100644 --- a/main.tf +++ b/main.tf @@ -1,28 +1,60 @@ locals { - enabled = "${var.enabled == "true" ? true : false }" - id = "${local.enabled ? lower(join(var.delimiter, compact(concat(list(var.namespace, var.stage, var.name), var.attributes)))) : ""}" - name = "${local.enabled ? lower(format("%v", var.name)) : ""}" - namespace = "${local.enabled ? lower(format("%v", var.namespace)) : ""}" - stage = "${local.enabled ? lower(format("%v", var.stage)) : ""}" - attributes = "${local.enabled ? lower(format("%v", join(var.delimiter, compact(var.attributes)))) : ""}" + enabled = "${var.enabled == "true" ? true : false }" - tags = "${ - merge( - map( - "Name", "${local.id}", - "Namespace", "${local.namespace}", - "Stage", "${local.stage}" - ), var.tags - ) - }" + id = "${lower(join(local.delimiter, compact(concat(list(local.namespace, local.environment, local.stage, local.name, local.attributes)))))}" - tags_as_list_of_maps = ["${null_resource.tags_as_list_of_maps.*.triggers}"] + # selected_name : Select which value to use, the one from context, or the one from the var + # name: Remove spaces, make lowercase + + selected_name = ["${concat(compact(concat(list(var.name), local.context_local["name"])), list(""))}"] + name = "${lower(replace(local.selected_name[0], "/[^a-zA-Z0-9]/", ""))}" + selected_namespace = ["${concat(compact(concat(local.context_local["namespace"], list(var.namespace))), list(""))}"] + namespace = "${lower(replace(local.selected_namespace[0], "/[^a-zA-Z0-9]/", ""))}" + selected_environment = ["${concat(compact(concat(local.context_local["environment"], list(var.environment))), list(""))}"] + environment = "${lower(replace(local.selected_environment[0], "/[^a-zA-Z0-9]/", ""))}" + selected_stage = ["${concat(compact(concat(local.context_local["stage"], list(var.stage))),list(""))}"] + stage = "${lower(replace(local.selected_stage[0], "/[^a-zA-Z0-9]/", ""))}" + selected_attributes = ["${distinct(compact(concat(var.attributes, local.context_local["attributes"])))}"] + attributes = "${lower(join(local.delimiter, local.selected_attributes))}" + selected_delimiter = ["${distinct(compact(concat(local.context_local["delimiter"], list(var.delimiter))))}"] + delimiter = "${local.selected_delimiter[0]}" + # Merge the map of empty values, with the variable context, so that context_local always contains all map keys + context_local = "${merge(local.context_struct, var.context)}" + # Only maps that contain all the same attribute types can be merged, so they have been set to list + context_struct = { + name = [] + namespace = [] + environment = [] + stage = [] + attributes = [] + tags_keys = [] + tags_values = [] + delimiter = [] + } + generated_tags = { + "Name" = "${local.id}" + "Namespace" = "${local.namespace}" + "Environment" = "${local.environment}" + "Stage" = "${local.stage}" + } + tags = "${merge(zipmap(local.context_local["tags_keys"], local.context_local["tags_values"]),local.generated_tags, var.tags )}" + tags_as_list_of_maps = ["${data.null_data_source.tags_as_list_of_maps.*.outputs}"] + context = { + name = ["${local.name}"] + namespace = ["${local.namespace}"] + environment = ["${local.environment}"] + stage = ["${local.stage}"] + attributes = ["${local.attributes}"] + tags_keys = ["${keys(local.tags)}"] + tags_values = ["${values(local.tags)}"] + delimiter = ["${local.delimiter}"] + } } -resource "null_resource" "tags_as_list_of_maps" { - count = "${length(keys(local.tags))}" +data "null_data_source" "tags_as_list_of_maps" { + count = "${local.enabled ? length(keys(local.tags)) : 0}" - triggers = "${merge(map( + inputs = "${merge(map( "key", "${element(keys(local.tags), count.index)}", "value", "${element(values(local.tags), count.index)}" ), diff --git a/outputs.tf b/outputs.tf index 36161db..b1d59cc 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,25 +1,25 @@ output "id" { - value = "${local.id}" + value = "${local.enabled ? local.id : ""}" description = "Disambiguated ID" } output "name" { - value = "${local.name}" + value = "${local.enabled ? local.name : ""}" description = "Normalized name" } output "namespace" { - value = "${local.namespace}" + value = "${local.enabled ? local.namespace : ""}" description = "Normalized namespace" } output "stage" { - value = "${local.stage}" + value = "${local.enabled ? local.stage : ""}" description = "Normalized stage" } output "attributes" { - value = "${local.attributes}" + value = "${local.enabled ? local.attributes : ""}" description = "Normalized attributes" } @@ -32,3 +32,13 @@ output "tags_as_list_of_maps" { value = ["${local.tags_as_list_of_maps}"] description = "Additional tags as a list of maps, which can be used in several AWS resources" } + +output "context" { + value = "${local.context}" + description = "Context of this module to pass between other modules" +} + +output "delimiter" { + value = "${local.delimiter}" + description = "Delimiter used in label ID" +} diff --git a/variables.tf b/variables.tf index eb0ed9c..eb207a6 100644 --- a/variables.tf +++ b/variables.tf @@ -1,13 +1,21 @@ variable "namespace" { description = "Namespace, which could be your organization name, e.g. 'cp' or 'cloudposse'" + default = "" +} + +variable "environment" { + description = "Environment, e.g. 'prod', 'staging', 'dev', 'pre-prod', 'UAT'" + default = "" } variable "stage" { - description = "Stage, e.g. 'prod', 'staging', 'dev', or 'test'" + description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" + default = "" } variable "name" { description = "Solution name, e.g. 'app' or 'jenkins'" + default = "" } variable "enabled" { @@ -38,3 +46,9 @@ variable "additional_tag_map" { default = {} description = "Additional tags for appending to each tag map." } + +variable "context" { + type = "map" + default = {} + description = "Default context to use for passing state between label invocations" +}