From 6a7c42ef2105764b96d97e7d9ab9578dd1d83dd5 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 1 Feb 2021 21:44:14 +0200 Subject: [PATCH] feat: add support for choosing letter case of context tags (#107) --- README.md | 8 +- docs/terraform.md | 6 +- examples/autoscalinggroup/context.tf | 44 ++++++++++ examples/autoscalinggroup/versions.tf | 2 +- examples/complete/context.tf | 44 ++++++++++ examples/complete/label8d.tf | 44 ++++++++++ examples/complete/label8dcd.tf | 24 ++++++ examples/complete/label8dnd.tf | 24 ++++++ examples/complete/label8l.tf | 46 +++++++++++ examples/complete/label8n.tf | 45 +++++++++++ examples/complete/label8t.tf | 45 +++++++++++ examples/complete/label8u.tf | 50 ++++++++++++ examples/complete/versions.tf | 2 +- exports/context.tf | 45 ++++++++++- main.tf | 67 ++++++++++----- test/src/examples_complete_test.go | 112 ++++++++++++++++++++++++++ test/src/go.sum | 1 + variables.tf | 44 ++++++++++ versions.tf | 2 +- 19 files changed, 627 insertions(+), 28 deletions(-) create mode 100644 examples/complete/label8d.tf create mode 100644 examples/complete/label8dcd.tf create mode 100644 examples/complete/label8dnd.tf create mode 100644 examples/complete/label8l.tf create mode 100644 examples/complete/label8n.tf create mode 100644 examples/complete/label8t.tf create mode 100644 examples/complete/label8u.tf diff --git a/README.md b/README.md index fea3fd9..050959b 100644 --- a/README.md +++ b/README.md @@ -658,7 +658,7 @@ Available targets: | Name | Version | |------|---------| -| terraform | >= 0.12.26 | +| terraform | >= 0.13.0 | ## Providers @@ -670,12 +670,14 @@ No provider. |------|-------------|------|---------|:--------:| | additional\_tag\_map | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | | attributes | Additional attributes (e.g. `1`) | `list(string)` | `[]` | no | -| 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. |
object({
enabled = bool
namespace = string
environment = string
stage = string
name = string
delimiter = string
attributes = list(string)
tags = map(string)
additional_tag_map = map(string)
regex_replace_chars = string
label_order = list(string)
id_length_limit = number
})
|
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_order": [],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | +| 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. |
object({
enabled = bool
namespace = string
environment = string
stage = string
name = string
delimiter = string
attributes = list(string)
tags = map(string)
additional_tag_map = map(string)
regex_replace_chars = string
label_order = list(string)
id_length_limit = number
label_key_case = string
label_value_case = string
})
|
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | | delimiter | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | enabled | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | environment | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | id\_length\_limit | Limit `id` to this many characters.
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | +| label\_key\_case | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | label\_order | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | +| label\_value\_case | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | | name | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | | namespace | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | | regex\_replace\_chars | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | @@ -794,7 +796,7 @@ In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. ## Copyright -Copyright © 2017-2020 [Cloud Posse, LLC](https://cpco.io/copyright) +Copyright © 2017-2021 [Cloud Posse, LLC](https://cpco.io/copyright) diff --git a/docs/terraform.md b/docs/terraform.md index 7fd57a4..23ee90f 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -3,7 +3,7 @@ | Name | Version | |------|---------| -| terraform | >= 0.12.26 | +| terraform | >= 0.13.0 | ## Providers @@ -15,12 +15,14 @@ No provider. |------|-------------|------|---------|:--------:| | additional\_tag\_map | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | | attributes | Additional attributes (e.g. `1`) | `list(string)` | `[]` | no | -| 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. |
object({
enabled = bool
namespace = string
environment = string
stage = string
name = string
delimiter = string
attributes = list(string)
tags = map(string)
additional_tag_map = map(string)
regex_replace_chars = string
label_order = list(string)
id_length_limit = number
})
|
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_order": [],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | +| 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. |
object({
enabled = bool
namespace = string
environment = string
stage = string
name = string
delimiter = string
attributes = list(string)
tags = map(string)
additional_tag_map = map(string)
regex_replace_chars = string
label_order = list(string)
id_length_limit = number
label_key_case = string
label_value_case = string
})
|
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | | delimiter | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | enabled | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | environment | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | id\_length\_limit | Limit `id` to this many characters.
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | +| label\_key\_case | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | label\_order | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | +| label\_value\_case | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | | name | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | | namespace | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | | regex\_replace\_chars | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | diff --git a/examples/autoscalinggroup/context.tf b/examples/autoscalinggroup/context.tf index cdc8e73..b97f05f 100644 --- a/examples/autoscalinggroup/context.tf +++ b/examples/autoscalinggroup/context.tf @@ -58,6 +58,8 @@ variable "context" { regex_replace_chars = string label_order = list(string) id_length_limit = number + label_key_case = string + label_value_case = string }) default = { enabled = true @@ -72,6 +74,8 @@ variable "context" { regex_replace_chars = null label_order = [] id_length_limit = null + label_key_case = null + label_value_case = null } description = <<-EOT Single object for setting entire context at once. @@ -80,6 +84,16 @@ variable "context" { Individual variable settings (non-null) override settings in context object, except for attributes, tags, and additional_tag_map, which are merged. EOT + + validation { + condition = var.context["label_key_case"] == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = var.context["label_value_case"] == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } } variable "enabled" { @@ -169,4 +183,34 @@ variable "id_length_limit" { EOT } +variable "label_key_case" { + type = string + default = null + description = <<-EOT + The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + The letter case of output label values (also used in `tags` and `id`). + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + #### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/autoscalinggroup/versions.tf b/examples/autoscalinggroup/versions.tf index f94b61f..450c502 100644 --- a/examples/autoscalinggroup/versions.tf +++ b/examples/autoscalinggroup/versions.tf @@ -1,3 +1,3 @@ terraform { - required_version = ">= 0.12.0" + required_version = ">= 0.13.0" } diff --git a/examples/complete/context.tf b/examples/complete/context.tf index cdc8e73..b97f05f 100644 --- a/examples/complete/context.tf +++ b/examples/complete/context.tf @@ -58,6 +58,8 @@ variable "context" { regex_replace_chars = string label_order = list(string) id_length_limit = number + label_key_case = string + label_value_case = string }) default = { enabled = true @@ -72,6 +74,8 @@ variable "context" { regex_replace_chars = null label_order = [] id_length_limit = null + label_key_case = null + label_value_case = null } description = <<-EOT Single object for setting entire context at once. @@ -80,6 +84,16 @@ variable "context" { Individual variable settings (non-null) override settings in context object, except for attributes, tags, and additional_tag_map, which are merged. EOT + + validation { + condition = var.context["label_key_case"] == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = var.context["label_value_case"] == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } } variable "enabled" { @@ -169,4 +183,34 @@ variable "id_length_limit" { EOT } +variable "label_key_case" { + type = string + default = null + description = <<-EOT + The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + The letter case of output label values (also used in `tags` and `id`). + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + #### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/complete/label8d.tf b/examples/complete/label8d.tf new file mode 100644 index 0000000..4252419 --- /dev/null +++ b/examples/complete/label8d.tf @@ -0,0 +1,44 @@ +module "label8d" { + source = "../../" + + enabled = true + namespace = "eg" + environment = "demo" + name = "blue" + attributes = ["cluster"] + delimiter = "-" + + tags = { + "kubernetes.io/cluster/" = "shared" + } +} + +module "label8d_context" { + source = "../../" + + context = module.label8d.context +} + +output "label8d_context_id" { + value = module.label8d_context.id +} + +output "label8d_context_context" { + value = module.label8d_context.context +} + +output "label8d_context_tags" { + value = module.label8d_context.tags +} + +output "label8d_id" { + value = module.label8d.id +} + +output "label8d_context" { + value = module.label8d.context +} + +output "label8d_tags" { + value = module.label8d.tags +} diff --git a/examples/complete/label8dcd.tf b/examples/complete/label8dcd.tf new file mode 100644 index 0000000..ccdae21 --- /dev/null +++ b/examples/complete/label8dcd.tf @@ -0,0 +1,24 @@ +module "label8dcd" { + source = "../../" + + enabled = true + namespace = "eg" + environment = "demo" + name = "blue" + attributes = ["cluster"] + delimiter = "x" +} + +module "label8dcd_context" { + source = "../../" + + context = module.label8dcd.context +} + +output "label8dcd_context_id" { + value = module.label8dcd_context.id +} + +output "label8dcd_id" { + value = module.label8dcd.id +} diff --git a/examples/complete/label8dnd.tf b/examples/complete/label8dnd.tf new file mode 100644 index 0000000..2edb378 --- /dev/null +++ b/examples/complete/label8dnd.tf @@ -0,0 +1,24 @@ +module "label8dnd" { + source = "../../" + + enabled = true + namespace = "eg" + environment = "demo" + name = "blue" + attributes = ["cluster"] + delimiter = "" +} + +module "label8dnd_context" { + source = "../../" + + context = module.label8dnd.context +} + +output "label8dnd_context_id" { + value = module.label8dnd_context.id +} + +output "label8dnd_id" { + value = module.label8dnd.id +} diff --git a/examples/complete/label8l.tf b/examples/complete/label8l.tf new file mode 100644 index 0000000..7462f9e --- /dev/null +++ b/examples/complete/label8l.tf @@ -0,0 +1,46 @@ +module "label8l" { + source = "../../" + enabled = true + namespace = "eg" + environment = "demo" + name = "blue" + attributes = ["cluster"] + delimiter = "-" + label_key_case = "lower" + label_value_case = "lower" + + tags = { + "kubernetes.io/cluster/" = "shared" + "upperTEST" = "testUPPER" + } +} + +module "label8l_context" { + source = "../../" + + context = module.label8l.context +} + +output "label8l_context_id" { + value = module.label8l_context.id +} + +output "label8l_context_context" { + value = module.label8l_context.context +} + +output "label8l_context_tags" { + value = module.label8l_context.tags +} + +output "label8l_id" { + value = module.label8l.id +} + +output "label8l_context" { + value = module.label8l.context +} + +output "label8l_tags" { + value = module.label8l.tags +} diff --git a/examples/complete/label8n.tf b/examples/complete/label8n.tf new file mode 100644 index 0000000..fd4ad57 --- /dev/null +++ b/examples/complete/label8n.tf @@ -0,0 +1,45 @@ +module "label8n" { + source = "../../" + + enabled = true + namespace = "EG" + environment = "demo" + name = "blue" + attributes = ["eks", "ClusteR"] + delimiter = "-" + label_value_case = "none" + + tags = { + "kubernetes.io/cluster/" = "shared" + } +} + +module "label8n_context" { + source = "../../" + + context = module.label8n.context +} + +output "label8n_context_id" { + value = module.label8n_context.id +} + +output "label8n_context_context" { + value = module.label8n_context.context +} + +output "label8n_context_tags" { + value = module.label8n_context.tags +} + +output "label8n_id" { + value = module.label8n.id +} + +output "label8n_context" { + value = module.label8n.context +} + +output "label8n_tags" { + value = module.label8n.tags +} diff --git a/examples/complete/label8t.tf b/examples/complete/label8t.tf new file mode 100644 index 0000000..cb54c7a --- /dev/null +++ b/examples/complete/label8t.tf @@ -0,0 +1,45 @@ +module "label8t" { + source = "../../" + enabled = true + namespace = "eg" + environment = "demo" + name = "blue" + attributes = ["EKS", "cluster"] + delimiter = "-" + label_key_case = "title" + label_value_case = "title" + + tags = { + "kubernetes.io/cluster/" = "shared" + } +} + +module "label8t_context" { + source = "../../" + + context = module.label8t.context +} + +output "label8t_context_id" { + value = module.label8t_context.id +} + +output "label8t_context_context" { + value = module.label8t_context.context +} + +output "label8t_context_tags" { + value = module.label8t_context.tags +} + +output "label8t_id" { + value = module.label8t.id +} + +output "label8t_context" { + value = module.label8t.context +} + +output "label8t_tags" { + value = module.label8t.tags +} diff --git a/examples/complete/label8u.tf b/examples/complete/label8u.tf new file mode 100644 index 0000000..499535b --- /dev/null +++ b/examples/complete/label8u.tf @@ -0,0 +1,50 @@ +module "label8u" { + source = "../../" + enabled = true + namespace = "eg" + environment = "demo" + name = "blue" + attributes = ["cluster"] + delimiter = "-" + label_key_case = "upper" + label_value_case = "upper" + + tags = { + "kubernetes.io/cluster/" = "shared" + } +} + +module "label8u_context" { + source = "../../" + + context = module.label8u.context +} + +output "label8u_context_id" { + value = module.label8u_context.id +} + +output "label8u_context_context" { + value = module.label8u_context.context +} + +// debug +output "label8u_context_normalized_context" { + value = module.label8u_context.normalized_context +} + +output "label8u_context_tags" { + value = module.label8u_context.tags +} + +output "label8u_id" { + value = module.label8u.id +} + +output "label8u_context" { + value = module.label8u.context +} + +output "label8u_tags" { + value = module.label8u.tags +} diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index f94b61f..450c502 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,3 +1,3 @@ terraform { - required_version = ">= 0.12.0" + required_version = ">= 0.13.0" } diff --git a/exports/context.tf b/exports/context.tf index f5f2797..ff90b1c 100644 --- a/exports/context.tf +++ b/exports/context.tf @@ -20,7 +20,7 @@ module "this" { source = "cloudposse/label/null" - version = "0.22.1" // requires Terraform >= 0.12.26 + version = "0.23.0" // requires Terraform >= 0.13.0 enabled = var.enabled namespace = var.namespace @@ -54,6 +54,8 @@ variable "context" { regex_replace_chars = string label_order = list(string) id_length_limit = number + label_key_case = string + label_value_case = string }) default = { enabled = true @@ -68,6 +70,8 @@ variable "context" { regex_replace_chars = null label_order = [] id_length_limit = null + label_key_case = null + label_value_case = null } description = <<-EOT Single object for setting entire context at once. @@ -76,6 +80,16 @@ variable "context" { Individual variable settings (non-null) override settings in context object, except for attributes, tags, and additional_tag_map, which are merged. EOT + + validation { + condition = var.context["label_key_case"] == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = var.context["label_value_case"] == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } } variable "enabled" { @@ -165,4 +179,33 @@ variable "id_length_limit" { EOT } +variable "label_key_case" { + type = string + default = null + description = <<-EOT + The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + The letter case of output label values (also used in `tags` and `id`). + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} #### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/main.tf b/main.tf index c9c73b8..cd5a6f3 100644 --- a/main.tf +++ b/main.tf @@ -5,15 +5,14 @@ locals { regex_replace_chars = "/[^-a-zA-Z0-9]/" delimiter = "-" replacement = "" - # The `sentinel` should match the `regex_replace_chars`, so it will be replaced with the `replacement` value - sentinel = "\t" - id_length_limit = 0 - id_hash_length = 5 + id_length_limit = 0 + id_hash_length = 5 + label_value_case = "lower" + label_key_case = "title" } - # So far, we have decided not to allow overriding replacement, sentinel, or id_hash_length + # So far, we have decided not to allow overriding replacement or id_hash_length replacement = local.defaults.replacement - sentinel = local.defaults.sentinel id_hash_length = local.defaults.id_hash_length # The values provided by variables supersede the values inherited from the context object, @@ -35,24 +34,44 @@ locals { label_order = var.label_order == null ? var.context.label_order : var.label_order regex_replace_chars = var.regex_replace_chars == null ? var.context.regex_replace_chars : var.regex_replace_chars id_length_limit = var.id_length_limit == null ? var.context.id_length_limit : var.id_length_limit + label_key_case = var.label_key_case == null ? var.context.label_key_case : var.label_key_case + label_value_case = var.label_value_case == null ? var.context.label_value_case : var.label_value_case } enabled = local.input.enabled regex_replace_chars = coalesce(local.input.regex_replace_chars, local.defaults.regex_replace_chars) - name = lower(replace(coalesce(local.input.name, local.sentinel), local.regex_replace_chars, local.replacement)) - namespace = lower(replace(coalesce(local.input.namespace, local.sentinel), local.regex_replace_chars, local.replacement)) - environment = lower(replace(coalesce(local.input.environment, local.sentinel), local.regex_replace_chars, local.replacement)) - stage = lower(replace(coalesce(local.input.stage, local.sentinel), local.regex_replace_chars, local.replacement)) - delimiter = local.input.delimiter == null ? local.defaults.delimiter : local.input.delimiter - label_order = local.input.label_order == null ? local.defaults.label_order : coalescelist(local.input.label_order, local.defaults.label_order) - id_length_limit = local.input.id_length_limit == null ? local.defaults.id_length_limit : local.input.id_length_limit + # string_label_names are names of inputs that are strings (not list of strings) used as labels + string_label_names = ["name", "namespace", "environment", "stage"] + normalized_labels = { for k in local.string_label_names : k => + local.input[k] == null ? "" : replace(local.input[k], local.regex_replace_chars, local.replacement) + } + normalized_attributes = compact(distinct([for v in local.input.attributes : replace(v, local.regex_replace_chars, local.replacement)])) + + formatted_labels = { for k in local.string_label_names : k => local.label_value_case == "none" ? local.normalized_labels[k] : + local.label_value_case == "title" ? title(lower(local.normalized_labels[k])) : + local.label_value_case == "upper" ? upper(local.normalized_labels[k]) : lower(local.normalized_labels[k]) + } + attributes = compact(distinct([ + for v in local.normalized_attributes : (local.label_value_case == "none" ? v : + local.label_value_case == "title" ? title(lower(v)) : + local.label_value_case == "upper" ? upper(v) : lower(v)) + ])) - additional_tag_map = merge(var.context.additional_tag_map, var.additional_tag_map) + name = local.formatted_labels["name"] + namespace = local.formatted_labels["namespace"] + environment = local.formatted_labels["environment"] + stage = local.formatted_labels["stage"] - attributes = local.input.attributes + delimiter = local.input.delimiter == null ? local.defaults.delimiter : local.input.delimiter + label_order = local.input.label_order == null ? local.defaults.label_order : coalescelist(local.input.label_order, local.defaults.label_order) + id_length_limit = local.input.id_length_limit == null ? local.defaults.id_length_limit : local.input.id_length_limit + label_key_case = local.input.label_key_case == null ? local.defaults.label_key_case : local.input.label_key_case + label_value_case = local.input.label_value_case == null ? local.defaults.label_value_case : local.input.label_value_case + + additional_tag_map = merge(var.context.additional_tag_map, var.additional_tag_map) tags = merge(local.generated_tags, local.input.tags) @@ -73,26 +92,34 @@ locals { attributes = local.id_context.attributes } - generated_tags = { for l in keys(local.tags_context) : title(l) => local.tags_context[l] if length(local.tags_context[l]) > 0 } + generated_tags = { + for l in keys(local.tags_context) : + local.label_key_case == "upper" ? upper(l) : ( + local.label_key_case == "lower" ? lower(l) : title(lower(l)) + ) => local.tags_context[l] if length(local.tags_context[l]) > 0 + } id_context = { name = local.name namespace = local.namespace environment = local.environment stage = local.stage - attributes = lower(replace(join(local.delimiter, local.attributes), local.regex_replace_chars, local.replacement)) + attributes = join(local.delimiter, local.attributes) } labels = [for l in local.label_order : local.id_context[l] if length(local.id_context[l]) > 0] - id_full = lower(join(local.delimiter, local.labels)) + id_full = join(local.delimiter, local.labels) # Create a truncated ID if needed delimiter_length = length(local.delimiter) # Calculate length of normal part of ID, leaving room for delimiter and hash id_truncated_length_limit = local.id_length_limit - (local.id_hash_length + local.delimiter_length) # Truncate the ID and ensure a single (not double) trailing delimiter id_truncated = local.id_truncated_length_limit <= 0 ? "" : "${trimsuffix(substr(local.id_full, 0, local.id_truncated_length_limit), local.delimiter)}${local.delimiter}" - id_hash = md5(local.id_full) + # Support usages that disallow numeric characters. Would prefer tr 0-9 q-z but Terraform does not support it. + id_hash_plus = "${md5(local.id_full)}qrstuvwxyz" + 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 + id_hash = replace(local.id_hash_case, local.regex_replace_chars, local.replacement) # Create the short ID by adding a hash to the end of the truncated ID id_short = substr("${local.id_truncated}${local.id_hash}", 0, local.id_length_limit) id = local.id_length_limit != 0 && length(local.id_full) > local.id_length_limit ? local.id_short : local.id_full @@ -112,6 +139,8 @@ locals { label_order = local.label_order regex_replace_chars = local.regex_replace_chars id_length_limit = local.id_length_limit + label_key_case = local.label_key_case + label_value_case = local.label_value_case } } diff --git a/test/src/examples_complete_test.go b/test/src/examples_complete_test.go index 5fc9341..322bdb3 100644 --- a/test/src/examples_complete_test.go +++ b/test/src/examples_complete_test.go @@ -177,4 +177,116 @@ func TestExamplesComplete(t *testing.T) { label7 := terraform.OutputMap(t, terraformOptions, "label7") assert.Equal(t, "eg-demo-blue-cluster-nodegroup", label7["id"], "var.attributes should be appended after context.attributes") + // Verify that apply with `label_key_case=title`, `label_value_case=lower`, `delimiter=""` returns expected value of id, context id + label8dndID := terraform.Output(t, terraformOptions, "label8dnd_id") + label8dndContextID := terraform.Output(t, terraformOptions, "label8dnd_context_id") + assert.Equal(t, "egdemobluecluster", label8dndID) + assert.Equal(t, label8dndID, label8dndContextID, "ID and context ID should be equal") + + // Verify that apply with `label_key_case=title`, `label_value_case=lower`, `delimiter="x"` returns expected value of id, context id + label8dcdID := terraform.Output(t, terraformOptions, "label8dcd_id") + label8dcdContextID := terraform.Output(t, terraformOptions, "label8dcd_context_id") + assert.Equal(t, "egxdemoxbluexcluster", label8dcdID) + assert.Equal(t, label8dcdID, label8dcdContextID, "ID and context ID should be equal") + + // Verify that apply with `label_key_case=title` and `label_value_case=lower` returns expected values of id, tags, context tags + label8dExpectedTags := map[string]string{ + "Attributes": "cluster", + "Environment": "demo", + "Name": "eg-demo-blue-cluster", + "Namespace": "eg", + "kubernetes.io/cluster/": "shared", + } + + label8dID := terraform.Output(t, terraformOptions, "label8d_id") + label8dContextID := terraform.Output(t, terraformOptions, "label8d_context_id") + assert.Equal(t, "eg-demo-blue-cluster", label8dID) + assert.Equal(t, label8dID, label8dContextID, "ID and context ID should be equal") + + label8dTags := terraform.OutputMap(t, terraformOptions, "label8d_tags") + label8dContextTags := terraform.OutputMap(t, terraformOptions, "label8d_context_tags") + + assert.Exactly(t, label8dExpectedTags, label8dTags, "generated tags are different from expected") + assert.Exactly(t, label8dTags, label8dContextTags, "tags and context tags should be equal") + + // Verify that apply with `label_key_case=lower` and `label_value_case=lower` returns expected values of id, tags, context tags + label8lExpectedTags := map[string]string{ + "attributes": "cluster", + "environment": "demo", + "name": "eg-demo-blue-cluster", + "namespace": "eg", + "kubernetes.io/cluster/": "shared", + "upperTEST": "testUPPER", + } + + label8lID := terraform.Output(t, terraformOptions, "label8l_id") + label8lContextID := terraform.Output(t, terraformOptions, "label8l_context_id") + assert.Equal(t, "eg-demo-blue-cluster", label8lID) + assert.Equal(t, label8lID, label8lContextID, "ID and context ID should be equal") + + label8lTags := terraform.OutputMap(t, terraformOptions, "label8l_tags") + label8lContextTags := terraform.OutputMap(t, terraformOptions, "label8l_context_tags") + + assert.Exactly(t, label8lExpectedTags, label8lTags, "generated tags are different from expected") + assert.Exactly(t, label8lTags, label8lContextTags, "tags and context tags should be equal") + + // Verify that apply with `label_key_case=title` and `label_value_case=title` returns expected values of id, tags, context tags + label8tExpectedTags := map[string]string{ + "Attributes": "Eks-Cluster", + "Environment": "Demo", + "Name": "Eg-Demo-Blue-Eks-Cluster", + "Namespace": "Eg", + "kubernetes.io/cluster/": "shared", + } + + label8tID := terraform.Output(t, terraformOptions, "label8t_id") + label8tContextID := terraform.Output(t, terraformOptions, "label8t_context_id") + assert.Equal(t, "Eg-Demo-Blue-Eks-Cluster", label8tID) + assert.Equal(t, label8tID, label8tContextID, "ID and context ID should be equal") + + label8tTags := terraform.OutputMap(t, terraformOptions, "label8t_tags") + label8tContextTags := terraform.OutputMap(t, terraformOptions, "label8t_context_tags") + + assert.Exactly(t, label8tExpectedTags, label8tTags, "generated tags are different from expected") + assert.Exactly(t, label8tTags, label8tContextTags, "tags and context tags should be equal") + + // Verify that apply with `label_key_case=upper` and `label_value_case=upper` returns expected values of id, tags, context tags + label8uExpectedTags := map[string]string{ + "ATTRIBUTES": "CLUSTER", + "ENVIRONMENT": "DEMO", + "NAME": "EG-DEMO-BLUE-CLUSTER", + "NAMESPACE": "EG", + "kubernetes.io/cluster/": "shared", + } + + label8uID := terraform.Output(t, terraformOptions, "label8u_id") + label8uContextID := terraform.Output(t, terraformOptions, "label8u_context_id") + assert.Equal(t, "EG-DEMO-BLUE-CLUSTER", label8uID) + assert.Equal(t, label8uID, label8uContextID, "ID and context ID should be equal") + + label8uTags := terraform.OutputMap(t, terraformOptions, "label8u_tags") + label8uContextTags := terraform.OutputMap(t, terraformOptions, "label8u_context_tags") + + assert.Exactly(t, label8uExpectedTags, label8uTags, "generated tags are different from expected") + assert.Exactly(t, label8uTags, label8uContextTags, "tags and context tags should be equal") + + // Verify that apply with `label_key_case=title` and `label_value_case=none` returns expected values of id, tags, context tags + label8nExpectedTags := map[string]string{ + "Attributes": "eks-ClusteR", + "Environment": "demo", + "Name": "EG-demo-blue-eks-ClusteR", + "Namespace": "EG", + "kubernetes.io/cluster/": "shared", + } + + label8nID := terraform.Output(t, terraformOptions, "label8n_id") + label8nContextID := terraform.Output(t, terraformOptions, "label8n_context_id") + assert.Equal(t, "EG-demo-blue-eks-ClusteR", label8nID) + assert.Equal(t, label8nID, label8nContextID, "ID and context ID should be equal") + + label8nTags := terraform.OutputMap(t, terraformOptions, "label8n_tags") + label8nContextTags := terraform.OutputMap(t, terraformOptions, "label8n_context_tags") + + assert.Exactly(t, label8nExpectedTags, label8nTags, "generated tags are different from expected") + assert.Exactly(t, label8nTags, label8nContextTags, "tags and context tags should be equal") } diff --git a/test/src/go.sum b/test/src/go.sum index d69a78e..b05e040 100644 --- a/test/src/go.sum +++ b/test/src/go.sum @@ -289,6 +289,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/variables.tf b/variables.tf index 438c84b..9ede816 100644 --- a/variables.tf +++ b/variables.tf @@ -12,6 +12,8 @@ variable "context" { regex_replace_chars = string label_order = list(string) id_length_limit = number + label_key_case = string + label_value_case = string }) default = { enabled = true @@ -26,6 +28,8 @@ variable "context" { regex_replace_chars = null label_order = [] id_length_limit = null + label_key_case = null + label_value_case = null } description = <<-EOT Single object for setting entire context at once. @@ -34,6 +38,16 @@ variable "context" { Individual variable settings (non-null) override settings in context object, except for attributes, tags, and additional_tag_map, which are merged. EOT + + validation { + condition = var.context["label_key_case"] == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = var.context["label_value_case"] == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } } variable "enabled" { @@ -122,3 +136,33 @@ variable "id_length_limit" { Does not affect `id_full`. EOT } + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + The letter case of output label values (also used in `tags` and `id`). + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} diff --git a/versions.tf b/versions.tf index 51de421..450c502 100644 --- a/versions.tf +++ b/versions.tf @@ -1,3 +1,3 @@ terraform { - required_version = ">= 0.12.26" + required_version = ">= 0.13.0" }