diff --git a/README.md b/README.md index b02ecf9e..0b01a980 100644 --- a/README.md +++ b/README.md @@ -356,14 +356,14 @@ Available targets: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 4.17.0 | +| [aws](#requirement\_aws) | >= 4.23.0 | | [null](#requirement\_null) | >= 2.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.17.0 | +| [aws](#provider\_aws) | >= 4.23.0 | ## Modules diff --git a/docs/terraform.md b/docs/terraform.md index 62546273..fc6cddbf 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -4,14 +4,14 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 4.17.0 | +| [aws](#requirement\_aws) | >= 4.23.0 | | [null](#requirement\_null) | >= 2.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.17.0 | +| [aws](#provider\_aws) | >= 4.23.0 | ## Modules diff --git a/examples/postgres/context.tf b/examples/postgres/context.tf new file mode 100644 index 00000000..5e0ef885 --- /dev/null +++ b/examples/postgres/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + 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. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + 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. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + 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. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + 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. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + 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. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + 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. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + 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. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + 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`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + 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`. + 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 + 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`. + 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`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + 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). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/examples/postgres/fixtures.us-east-2.tfvars b/examples/postgres/fixtures.us-east-2.tfvars new file mode 100644 index 00000000..f6d528f8 --- /dev/null +++ b/examples/postgres/fixtures.us-east-2.tfvars @@ -0,0 +1,43 @@ +region = "us-east-1" + +availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"] + +namespace = "eg" + +stage = "test" + +name = "rds-cluster" + +instance_type = "db.m5d.large" + +cluster_family = "postgres13" + +cluster_size = 1 + +deletion_protection = false + +autoscaling_enabled = false + +engine = "postgres" + +engine_mode = "provisioned" + +engine_version = "13.4" + +db_name = "test_db" + +admin_user = "admin_test" + +admin_password = "admin_password" + +enhanced_monitoring_role_enabled = true + +rds_monitoring_interval = 30 + +allocated_storage = 100 + +storage_type = "io1" + +iops = 1000 + +db_cluster_instance_class = "db.m5d.large" \ No newline at end of file diff --git a/examples/postgres/main.tf b/examples/postgres/main.tf new file mode 100644 index 00000000..32f28bc2 --- /dev/null +++ b/examples/postgres/main.tf @@ -0,0 +1,51 @@ +provider "aws" { + region = var.region +} + +module "vpc" { + source = "cloudposse/vpc/aws" + version = "1.1.0" + + ipv4_primary_cidr_block = "172.16.0.0/16" + + context = module.this.context +} + +module "subnets" { + source = "cloudposse/dynamic-subnets/aws" + version = "2.0.2" + + availability_zones = var.availability_zones + vpc_id = module.vpc.vpc_id + igw_id = [module.vpc.igw_id] + ipv4_cidr_block = [module.vpc.vpc_cidr_block] + nat_gateway_enabled = false + nat_instance_enabled = false + + context = module.this.context +} + +module "rds_cluster" { + source = "../../" + + engine = var.engine + engine_mode = var.engine_mode + engine_version = var.engine_version + cluster_family = var.cluster_family + cluster_size = contains(regex("^(?:.*(aurora))?.*$", var.engine), "aurora") ? var.cluster_size : 0 + admin_user = var.admin_user + admin_password = var.admin_password + db_name = var.db_name + instance_type = var.instance_type + db_cluster_instance_class = var.db_cluster_instance_class + vpc_id = module.vpc.vpc_id + subnets = module.subnets.private_subnet_ids + security_groups = [module.vpc.vpc_default_security_group_id] + deletion_protection = var.deletion_protection + autoscaling_enabled = var.autoscaling_enabled + storage_type = var.storage_type + iops = var.iops + allocated_storage = var.allocated_storage + + context = module.this.context +} diff --git a/examples/postgres/outputs.tf b/examples/postgres/outputs.tf new file mode 100644 index 00000000..6e863008 --- /dev/null +++ b/examples/postgres/outputs.tf @@ -0,0 +1,74 @@ +output "database_name" { + value = module.rds_cluster.database_name + description = "Database name" +} + +output "cluster_identifier" { + value = module.rds_cluster.cluster_identifier + description = "Cluster Identifier" +} + +output "arn" { + value = module.rds_cluster.arn + description = "Amazon Resource Name (ARN) of the cluster" +} + +output "endpoint" { + value = module.rds_cluster.endpoint + description = "The DNS address of the RDS instance" +} + +output "reader_endpoint" { + value = module.rds_cluster.reader_endpoint + description = "A read-only endpoint for the Aurora cluster, automatically load-balanced across replicas" +} + +output "master_host" { + value = module.rds_cluster.master_host + description = "DB Master hostname" +} + +output "replicas_host" { + value = module.rds_cluster.replicas_host + description = "Replicas hostname" +} + +output "dbi_resource_ids" { + value = module.rds_cluster.dbi_resource_ids + description = "List of the region-unique, immutable identifiers for the DB instances in the cluster" +} + +output "cluster_resource_id" { + value = module.rds_cluster.cluster_resource_id + description = "The region-unique, immutable identifie of the cluster" +} + +output "public_subnet_cidrs" { + value = module.subnets.public_subnet_cidrs + description = "Public subnet CIDR blocks" +} + +output "private_subnet_cidrs" { + value = module.subnets.private_subnet_cidrs + description = "Private subnet CIDR blocks" +} + +output "vpc_cidr" { + value = module.vpc.vpc_cidr_block + description = "VPC CIDR" +} + +output "security_group_id" { + value = module.rds_cluster.security_group_id + description = "Security Group ID" +} + +output "security_group_arn" { + value = module.rds_cluster.security_group_arn + description = "Security Group ARN" +} + +output "security_group_name" { + value = module.rds_cluster.security_group_name + description = "Security Group name" +} diff --git a/examples/postgres/variables.tf b/examples/postgres/variables.tf new file mode 100644 index 00000000..f5d6c47e --- /dev/null +++ b/examples/postgres/variables.tf @@ -0,0 +1,98 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "availability_zones" { + type = list(string) +} + +variable "instance_type" { + type = string + description = "Instance type to use" +} + +variable "cluster_size" { + type = number + description = "Number of DB instances to create in the cluster" +} + +variable "db_name" { + type = string + description = "Database name" +} + +variable "admin_user" { + type = string + description = "(Required unless a snapshot_identifier is provided) Username for the master DB user" +} + +variable "admin_password" { + type = string + description = "(Required unless a snapshot_identifier is provided) Password for the master DB user" +} + +variable "cluster_family" { + type = string + description = "The family of the DB cluster parameter group" +} + +variable "engine" { + type = string + description = "The name of the database engine to be used for this DB cluster. Valid values: `aurora`, `aurora-mysql`, `aurora-postgresql`" +} + +variable "engine_mode" { + type = string + description = "The database engine mode. Valid values: `parallelquery`, `provisioned`, `serverless`" +} + +variable "engine_version" { + type = string + default = "" + description = "The version of the database engine to use. See `aws rds describe-db-engine-versions` " +} + +variable "deletion_protection" { + type = bool + description = "If the DB instance should have deletion protection enabled" +} + +variable "autoscaling_enabled" { + type = bool + description = "Whether to enable cluster autoscaling" +} + +variable "enhanced_monitoring_role_enabled" { + type = bool + description = "A boolean flag to enable/disable the creation of the enhanced monitoring IAM role. If set to `false`, the module will not create a new role and will use `rds_monitoring_role_arn` for enhanced monitoring" +} + +variable "rds_monitoring_interval" { + type = number + description = "The interval, in seconds, between points when enhanced monitoring metrics are collected for the DB instance. To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. Valid Values: 0, 1, 5, 10, 15, 30, 60" +} + +variable "storage_type" { + type = string + description = "One of 'standard' (magnetic), 'gp2' (general purpose SSD), or 'io1' (provisioned IOPS SSD)" + default = null +} + +variable "iops" { + type = number + description = "The amount of provisioned IOPS. Setting this implies a storage_type of 'io1'. This setting is required to create a Multi-AZ DB cluster. Check TF docs for values based on db engine" + default = null +} + +variable "allocated_storage" { + type = number + description = "The allocated storage in GBs" + default = null +} + +variable "db_cluster_instance_class" { + type = string + default = "db.m5d.large" + description = "This setting is required to create a Multi-AZ DB cluste" +} \ No newline at end of file diff --git a/examples/postgres/versions.tf b/examples/postgres/versions.tf new file mode 100644 index 00000000..67af3b93 --- /dev/null +++ b/examples/postgres/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.17.0" + } + null = { + source = "hashicorp/null" + version = ">= 2.0" + } + } +} diff --git a/test/src/examples_postgres_test.go b/test/src/examples_postgres_test.go new file mode 100644 index 00000000..5e0e8223 --- /dev/null +++ b/test/src/examples_postgres_test.go @@ -0,0 +1,100 @@ +package test + +import ( + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + testStructure "github.com/gruntwork-io/terratest/modules/test-structure" + "github.com/stretchr/testify/assert" +) + +// Test the Terraform module in examples/complete using Terratest. +func TestExamplesPostgres(t *testing.T) { + t.Parallel() + randID := strings.ToLower(random.UniqueId()) + attributes := []string{randID} + + rootFolder := "../../" + terraformFolderRelativeToRoot := "examples/postgres" + varFiles := []string{"fixtures.us-east-2.tfvars"} + + tempTestFolder := testStructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: tempTestFolder, + Upgrade: true, + // Variables to pass to our Terraform code using -var-file options + VarFiles: varFiles, + Vars: map[string]interface{}{ + "attributes": attributes, + }, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer cleanup(t, terraformOptions, tempTestFolder) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + + // Run `terraform output` to get the value of an output variable + vpcCidr := terraform.Output(t, terraformOptions, "vpc_cidr") + // Verify we're getting back the outputs we expect + assert.Equal(t, "172.16.0.0/16", vpcCidr) + + // Run `terraform output` to get the value of an output variable + privateSubnetCidrs := terraform.OutputList(t, terraformOptions, "private_subnet_cidrs") + // Verify we're getting back the outputs we expect + assert.Equal(t, []string{"172.16.0.0/20", "172.16.16.0/20", "172.16.32.0/20"}, privateSubnetCidrs) + + // Run `terraform output` to get the value of an output variable + publicSubnetCidrs := terraform.OutputList(t, terraformOptions, "public_subnet_cidrs") + // Verify we're getting back the outputs we expect + assert.Equal(t, []string{"172.16.96.0/20", "172.16.112.0/20", "172.16.128.0/20"}, publicSubnetCidrs) + + // Run `terraform output` to get the value of an output variable + clusterIdentifier := terraform.Output(t, terraformOptions, "cluster_identifier") + expectedClusterIdentifier := "eg-test-rds-cluster-" + randID + // Verify we're getting back the outputs we expect + assert.Equal(t, expectedClusterIdentifier, clusterIdentifier) + + // Run `terraform output` to get the value of an output variable + arn := terraform.Output(t, terraformOptions, "arn") + // Verify we're getting back the outputs we expect + assert.Contains(t, arn, ":cluster:eg-test-rds-cluster") +} + +func TestExamplesPostgresDisabled(t *testing.T) { + t.Parallel() + randID := strings.ToLower(random.UniqueId()) + attributes := []string{randID} + + rootFolder := "../../" + terraformFolderRelativeToRoot := "examples/postgres" + varFiles := []string{"fixtures.us-east-2.tfvars"} + + tempTestFolder := testStructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot) + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: tempTestFolder, + Upgrade: true, + // Variables to pass to our Terraform code using -var-file options + VarFiles: varFiles, + Vars: map[string]interface{}{ + "attributes": attributes, + "enabled": "false", + }, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer cleanup(t, terraformOptions, tempTestFolder) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + results := terraform.InitAndApply(t, terraformOptions) + + // Should complete successfully without creating or changing any resources + assert.Contains(t, results, "Resources: 0 added, 0 changed, 0 destroyed.") +} diff --git a/versions.tf b/versions.tf index b6483d5a..375a8719 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.17.0" + version = ">= 4.23.0" } null = { source = "hashicorp/null"