From f3b14f5e694f83c21da0b9865fbb0e5135ea342e Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Mon, 9 Dec 2024 10:44:15 +0100 Subject: [PATCH] Add support for password validation policy to cloudsql module (#2740) * add support for password validation policy to cloudsql module * fix defaults --- modules/cloudsql-instance/README.md | 21 ++-- modules/cloudsql-instance/main.tf | 132 ++++++++++++++++++------- modules/cloudsql-instance/variables.tf | 13 +++ 3 files changed, 122 insertions(+), 44 deletions(-) diff --git a/modules/cloudsql-instance/README.md b/modules/cloudsql-instance/README.md index 7bbf24f833..5520ce654d 100644 --- a/modules/cloudsql-instance/README.md +++ b/modules/cloudsql-instance/README.md @@ -369,9 +369,9 @@ module "db" { | [database_version](variables.tf#L75) | Database type and version to create. | string | ✓ | | | [name](variables.tf#L179) | Name of primary instance. | string | ✓ | | | [network_config](variables.tf#L184) | Network configuration for the instance. Only one between private_network and psc_config can be used. | object({…}) | ✓ | | -| [project_id](variables.tf#L218) | The ID of the project where this instances will be created. | string | ✓ | | -| [region](variables.tf#L223) | Region of the primary instance. | string | ✓ | | -| [tier](variables.tf#L266) | The machine type to use for the instances. | string | ✓ | | +| [project_id](variables.tf#L231) | The ID of the project where this instances will be created. | string | ✓ | | +| [region](variables.tf#L236) | Region of the primary instance. | string | ✓ | | +| [tier](variables.tf#L279) | The machine type to use for the instances. | string | ✓ | | | [activation_policy](variables.tf#L16) | This variable specifies when the instance should be active. Can be either ALWAYS, NEVER or ON_DEMAND. Default is ALWAYS. | string | | "ALWAYS" | | [availability_type](variables.tf#L27) | Availability type for the primary replica. Either `ZONAL` or `REGIONAL`. | string | | "ZONAL" | | [backup_configuration](variables.tf#L33) | Backup settings for primary instance. Will be automatically enabled if using MySQL with one or more replicas. | object({…}) | | {…} | @@ -389,13 +389,14 @@ module "db" { | [insights_config](variables.tf#L129) | Query Insights configuration. Defaults to null which disables Query Insights. | object({…}) | | null | | [labels](variables.tf#L140) | Labels to be attached to all instances. | map(string) | | null | | [maintenance_config](variables.tf#L146) | Set maintenance window configuration and maintenance deny period (up to 90 days). Date format: 'yyyy-mm-dd'. | object({…}) | | {} | -| [prefix](variables.tf#L208) | Optional prefix used to generate instance names. | string | | null | -| [replicas](variables.tf#L228) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | map(object({…})) | | {} | -| [root_password](variables.tf#L238) | Root password of the Cloud SQL instance. Required for MS SQL Server. | string | | null | -| [ssl](variables.tf#L244) | Setting to enable SSL, set config and certificates. | object({…}) | | {} | -| [terraform_deletion_protection](variables.tf#L259) | Prevent terraform from deleting instances. | bool | | true | -| [time_zone](variables.tf#L271) | The time_zone to be used by the database engine (supported only for SQL Server), in SQL Server timezone format. | string | | null | -| [users](variables.tf#L277) | Map of users to create in the primary instance (and replicated to other replicas). For MySQL, anything after the first `@` (if present) will be used as the user's host. Set PASSWORD to null if you want to get an autogenerated password. The user types available are: 'BUILT_IN', 'CLOUD_IAM_USER' or 'CLOUD_IAM_SERVICE_ACCOUNT'. | map(object({…})) | | null | +| [password_validation_policy](variables.tf#L207) | Password validation policy configuration for instances. | object({…}) | | null | +| [prefix](variables.tf#L221) | Optional prefix used to generate instance names. | string | | null | +| [replicas](variables.tf#L241) | Map of NAME=> {REGION, KMS_KEY} for additional read replicas. Set to null to disable replica creation. | map(object({…})) | | {} | +| [root_password](variables.tf#L251) | Root password of the Cloud SQL instance. Required for MS SQL Server. | string | | null | +| [ssl](variables.tf#L257) | Setting to enable SSL, set config and certificates. | object({…}) | | {} | +| [terraform_deletion_protection](variables.tf#L272) | Prevent terraform from deleting instances. | bool | | true | +| [time_zone](variables.tf#L284) | The time_zone to be used by the database engine (supported only for SQL Server), in SQL Server timezone format. | string | | null | +| [users](variables.tf#L290) | Map of users to create in the primary instance (and replicated to other replicas). For MySQL, anything after the first `@` (if present) will be used as the user's host. Set PASSWORD to null if you want to get an autogenerated password. The user types available are: 'BUILT_IN', 'CLOUD_IAM_USER' or 'CLOUD_IAM_SERVICE_ACCOUNT'. | map(object({…})) | | null | ## Outputs diff --git a/modules/cloudsql-instance/main.tf b/modules/cloudsql-instance/main.tf index 98de9eb78f..b42b11e8c1 100644 --- a/modules/cloudsql-instance/main.tf +++ b/modules/cloudsql-instance/main.tf @@ -20,28 +20,29 @@ locals { is_postgres = can(regex("^POSTGRES", var.database_version)) has_replicas = length(var.replicas) > 0 is_regional = var.availability_type == "REGIONAL" ? true : false - - // Enable backup if the user asks for it or if the user is deploying - // MySQL in HA configuration (regional or with specified replicas) - enable_backup = var.backup_configuration.enabled || (local.is_mysql && local.has_replicas) || (local.is_mysql && local.is_regional) - + # enable backup if the user asks for it or if the user is deploying + # MySQL in HA configuration (regional or with specified replicas) + enable_backup = ( + var.backup_configuration.enabled || + (local.is_mysql && local.has_replicas) || + (local.is_mysql && local.is_regional) + ) users = { - for k, v in coalesce(var.users, {}) : - k => - local.is_mysql ? - { + for k, v in coalesce(var.users, {}) : k => + local.is_mysql + ? { name = coalesce(v.type, "BUILT_IN") == "BUILT_IN" ? split("@", k)[0] : k host = coalesce(v.type, "BUILT_IN") == "BUILT_IN" ? try(split("@", k)[1], null) : null password = coalesce(v.type, "BUILT_IN") == "BUILT_IN" ? try(random_password.passwords[k].result, v.password) : null type = coalesce(v.type, "BUILT_IN") - } : { + } + : { name = local.is_postgres ? try(trimsuffix(k, ".gserviceaccount.com"), k) : k host = null password = coalesce(v.type, "BUILT_IN") == "BUILT_IN" ? try(random_password.passwords[k].result, v.password) : null type = coalesce(v.type, "BUILT_IN") } } - } resource "google_sql_database_instance" "primary" { @@ -69,13 +70,23 @@ resource "google_sql_database_instance" "primary" { time_zone = var.time_zone ip_configuration { - ipv4_enabled = var.network_config.connectivity.public_ipv4 - private_network = try(var.network_config.connectivity.psa_config.private_network, null) - allocated_ip_range = try(var.network_config.connectivity.psa_config.allocated_ip_ranges.primary, null) - ssl_mode = var.ssl.ssl_mode - enable_private_path_for_google_cloud_services = var.network_config.connectivity.enable_private_path_for_services + ipv4_enabled = var.network_config.connectivity.public_ipv4 + private_network = try( + var.network_config.connectivity.psa_config.private_network, null + ) + allocated_ip_range = try( + var.network_config.connectivity.psa_config.allocated_ip_ranges.primary, null + ) + ssl_mode = var.ssl.ssl_mode + enable_private_path_for_google_cloud_services = ( + var.network_config.connectivity.enable_private_path_for_services + ) dynamic "authorized_networks" { - for_each = var.network_config.authorized_networks != null ? var.network_config.authorized_networks : {} + for_each = ( + var.network_config.authorized_networks != null + ? var.network_config.authorized_networks + : {} + ) iterator = network content { name = network.key @@ -83,10 +94,16 @@ resource "google_sql_database_instance" "primary" { } } dynamic "psc_config" { - for_each = var.network_config.connectivity.psc_allowed_consumer_projects != null ? [""] : [] + for_each = ( + var.network_config.connectivity.psc_allowed_consumer_projects != null + ? [""] + : [] + ) content { - psc_enabled = true - allowed_consumer_projects = var.network_config.connectivity.psc_allowed_consumer_projects + psc_enabled = true + allowed_consumer_projects = ( + var.network_config.connectivity.psc_allowed_consumer_projects + ) } } } @@ -95,7 +112,6 @@ resource "google_sql_database_instance" "primary" { for_each = local.enable_backup ? { 1 = 1 } : {} content { enabled = true - // enable binary log if the user asks for it or we have replicas (default in regional), // but only for MySQL binary_log_enabled = ( @@ -103,10 +119,14 @@ resource "google_sql_database_instance" "primary" { ? var.backup_configuration.binary_log_enabled || local.has_replicas || local.is_regional : null ) - start_time = var.backup_configuration.start_time - location = var.backup_configuration.location - point_in_time_recovery_enabled = var.backup_configuration.point_in_time_recovery_enabled - transaction_log_retention_days = var.backup_configuration.log_retention_days + start_time = var.backup_configuration.start_time + location = var.backup_configuration.location + point_in_time_recovery_enabled = ( + var.backup_configuration.point_in_time_recovery_enabled + ) + transaction_log_retention_days = ( + var.backup_configuration.log_retention_days + ) backup_retention_settings { retained_backups = var.backup_configuration.retention_count retention_unit = "COUNT" @@ -158,6 +178,28 @@ resource "google_sql_database_instance" "primary" { update_track = var.maintenance_config.maintenance_window.update_track } } + + dynamic "password_validation_policy" { + for_each = var.password_validation_policy != null ? [""] : [] + content { + complexity = ( + var.password_validation_policy.default_complexity == true + ? "COMPLEXITY_DEFAULT" + : null # "COMPLEXITY_UNSPECIFIED" generates a permadiff + ) + disallow_username_substring = ( + var.password_validation_policy.disallow_username_substring + ) + enable_password_policy = var.password_validation_policy.enabled + min_length = var.password_validation_policy.min_length + password_change_interval = ( + var.password_validation_policy.change_interval == null + ? null + : "${var.password_validation_policy.change_interval}s" + ) + reuse_interval = var.password_validation_policy.reuse_interval + } + } } deletion_protection = var.terraform_deletion_protection } @@ -183,12 +225,24 @@ resource "google_sql_database_instance" "replicas" { activation_policy = var.activation_policy ip_configuration { - ipv4_enabled = var.network_config.connectivity.public_ipv4 - private_network = try(var.network_config.connectivity.psa_config.private_network, null) - allocated_ip_range = try(var.network_config.connectivity.psa_config.allocated_ip_ranges.replica, null) - enable_private_path_for_google_cloud_services = var.network_config.connectivity.enable_private_path_for_services + ipv4_enabled = ( + var.network_config.connectivity.public_ipv4 + ) + private_network = ( + try(var.network_config.connectivity.psa_config.private_network, null) + ) + allocated_ip_range = try( + var.network_config.connectivity.psa_config.allocated_ip_ranges.replica, null + ) + enable_private_path_for_google_cloud_services = ( + var.network_config.connectivity.enable_private_path_for_services + ) dynamic "authorized_networks" { - for_each = var.network_config.authorized_networks != null ? var.network_config.authorized_networks : {} + for_each = ( + var.network_config.authorized_networks != null + ? var.network_config.authorized_networks + : {} + ) iterator = network content { name = network.key @@ -196,10 +250,16 @@ resource "google_sql_database_instance" "replicas" { } } dynamic "psc_config" { - for_each = var.network_config.connectivity.psc_allowed_consumer_projects != null ? [""] : [] + for_each = ( + var.network_config.connectivity.psc_allowed_consumer_projects != null + ? [""] + : [] + ) content { - psc_enabled = true - allowed_consumer_projects = var.network_config.connectivity.psc_allowed_consumer_projects + psc_enabled = true + allowed_consumer_projects = ( + var.network_config.connectivity.psc_allowed_consumer_projects + ) } } } @@ -249,7 +309,11 @@ moved { } resource "google_sql_ssl_cert" "client_certificates" { - for_each = var.ssl.client_certificates != null ? toset(var.ssl.client_certificates) : toset([]) + for_each = ( + var.ssl.client_certificates != null + ? toset(var.ssl.client_certificates) + : toset([]) + ) provider = google-beta project = var.project_id instance = google_sql_database_instance.primary.name diff --git a/modules/cloudsql-instance/variables.tf b/modules/cloudsql-instance/variables.tf index 2b8f6449c8..e47c0bb183 100644 --- a/modules/cloudsql-instance/variables.tf +++ b/modules/cloudsql-instance/variables.tf @@ -204,6 +204,19 @@ variable "network_config" { } } +variable "password_validation_policy" { + description = "Password validation policy configuration for instances." + type = object({ + enabled = optional(bool, true) + # change interval is only supported for postgresql + change_interval = optional(number) + default_complexity = optional(bool) + disallow_username_substring = optional(bool) + min_length = optional(number) + reuse_interval = optional(number) + }) + default = null +} variable "prefix" { description = "Optional prefix used to generate instance names."