locals { # Маппинг наших типов на Kubernetes типы k8s_secret_type_map = { "dockerconfigjson" = "kubernetes.io/dockerconfigjson" "database" = "Opaque" "s3" = "Opaque" "yc_sa" = "Opaque" "rabbitmq" = "Opaque" "kafka" = "Opaque" "opaque" = "Opaque" } default_yc_sa_key_names = { access_key = "access_key" secret_key = "secret_key" service_account_id = "service_account_id" } secrets_map = { for idx, secret in var.secrets : (try(secret.resource_key, "") != "" ? secret.resource_key : secret.name) => secret } random_keys_flat = merge([ for secret_name, secret in local.secrets_map : { for key, config in try(secret.random_keys, {}) : "${secret_name}:${key}" => { secret_name = secret_name key = key length = try(config.length, 32) special = try(config.special, false) } } ]...) database_outputs_by_secret = { for name, secret in local.secrets_map : name => try( var.database_outputs_map["${try(secret.dependencies.cluster, "")}:${try(secret.dependencies.db, "")}:${try(secret.dependencies.user, "")}"], null ) } rabbitmq_outputs_by_secret = { for name, secret in local.secrets_map : name => try( var.rabbitmq_outputs_map[ format( "%s:%s:%s", try(secret.dependencies.rabbitmq_ref, "default"), try(secret.dependencies.rabbitmq_vhost, try(secret.dependencies.vhost, "")), try(secret.dependencies.rabbitmq_user, try(secret.dependencies.user, "")) ) ], null ) } kafka_outputs_by_secret = { for name, secret in local.secrets_map : name => try( var.kafka_outputs_map[ format( "%s:%s", try(secret.dependencies.kafka_ref, try(secret.dependencies.cluster_ref, try(secret.dependencies.cluster, ""))), try(secret.dependencies.kafka_user, try(secret.dependencies.user, "")) ) ], null ) } rabbitmq_default_credentials_by_secret = { for name, secret in local.secrets_map : name => local.rabbitmq_outputs_by_secret[name] == null ? {} : { host = local.rabbitmq_outputs_by_secret[name].host hostname = "${local.rabbitmq_outputs_by_secret[name].host}:${local.rabbitmq_outputs_by_secret[name].port}" port = local.rabbitmq_outputs_by_secret[name].port vhost = local.rabbitmq_outputs_by_secret[name].vhost user = local.rabbitmq_outputs_by_secret[name].username username = local.rabbitmq_outputs_by_secret[name].username password = local.rabbitmq_outputs_by_secret[name].password uri = local.rabbitmq_outputs_by_secret[name].uri management_endpoint = local.rabbitmq_outputs_by_secret[name].management_endpoint } if secret.secret_type == "rabbitmq" } rabbitmq_credentials_by_secret = { for name, secret in local.secrets_map : name => length(try(secret.credential_keys, {})) == 0 ? local.rabbitmq_default_credentials_by_secret[name] : { for key, source in try(secret.credential_keys, {}) : key => try(local.rabbitmq_default_credentials_by_secret[name][source], "") } if secret.secret_type == "rabbitmq" && local.rabbitmq_outputs_by_secret[name] != null } kafka_default_credentials_by_secret = { for name, secret in local.secrets_map : name => local.kafka_outputs_by_secret[name] == null ? {} : { host = local.kafka_outputs_by_secret[name].host hostname = local.kafka_outputs_by_secret[name].hostname port = local.kafka_outputs_by_secret[name].port username = local.kafka_outputs_by_secret[name].username user = local.kafka_outputs_by_secret[name].username password = local.kafka_outputs_by_secret[name].password sasl_mechanism = local.kafka_outputs_by_secret[name].sasl_mechanism security_protocol = local.kafka_outputs_by_secret[name].security_protocol bootstrap_server = local.kafka_outputs_by_secret[name].bootstrap_server bootstrap_servers = local.kafka_outputs_by_secret[name].bootstrap_servers bootstrap_servers_json = local.kafka_outputs_by_secret[name].bootstrap_servers_json } if secret.secret_type == "kafka" } kafka_credentials_by_secret = { for name, secret in local.secrets_map : name => length(try(secret.credential_keys, {})) == 0 ? local.kafka_default_credentials_by_secret[name] : { for key, source in try(secret.credential_keys, {}) : key => try(local.kafka_default_credentials_by_secret[name][source], "") } if secret.secret_type == "kafka" && local.kafka_outputs_by_secret[name] != null } yc_sa_sources = { for name, secret in local.secrets_map : name => try( var.yc_service_accounts_map[try(secret.dependencies.service_account, "")], null ) if secret.secret_type == "yc_sa" } yc_sa_key_names_by_secret = { for name, secret in local.secrets_map : name => merge( local.default_yc_sa_key_names, try(secret.yc_sa_key_names, {}) ) if secret.secret_type == "yc_sa" } # Значения, захваченные из live-кластера (adopt 1:1). Приходят из расшифрованного # secret-values файла как map(resource_key => map(key => base64(raw_value))). # k8s хранит .data уже в base64, поэтому base64decode даёт исходное значение байт-в-байт. secrets_data_override = { for name, secret in local.secrets_map : name => { for key, b64 in try(var.secret_values[name], {}) : key => base64decode(b64) } if length(try(var.secret_values[name], {})) > 0 } secrets_data_computed = { for name, secret in local.secrets_map : name => secret.secret_type == "dockerconfigjson" ? { ".dockerconfigjson" = jsonencode({ auths = { "${secret.registry_url}" = { username = try(var.env_vars.DOCKER_REGISTRY_USERNAME, "") password = try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "") auth = base64encode("${try(var.env_vars.DOCKER_REGISTRY_USERNAME, "")}:${try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "")}") } } }) } : secret.secret_type == "s3" ? { # Если указан конкретный bucket в dependencies - используем его, иначе fallback на s3_outputs access_key = try(var.s3_buckets_map[secret.dependencies.bucket].access_key, var.s3_outputs.access_key) secret_key = try(var.s3_buckets_map[secret.dependencies.bucket].secret_key, var.s3_outputs.secret_key) bucket = try(var.s3_buckets_map[secret.dependencies.bucket].bucket_name, var.s3_outputs.bucket_name) endpoint = try(var.constants.s3_endpoint, "https://storage.yandexcloud.net") } : secret.secret_type == "yc_sa" ? merge( { (local.yc_sa_key_names_by_secret[name].access_key) = try(local.yc_sa_sources[name].access_key, "") (local.yc_sa_key_names_by_secret[name].secret_key) = try(local.yc_sa_sources[name].secret_key, "") (local.yc_sa_key_names_by_secret[name].service_account_id) = try(local.yc_sa_sources[name].service_account_id, "") endpoint = "https://storage.yandexcloud.net" }, try(secret.custom_keys, {}), { for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "") }, { for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result } ) : secret.secret_type == "database" && local.database_outputs_by_secret[name] != null ? merge( # Базовые поля из database outputs { host = local.database_outputs_by_secret[name].host port = try(var.constants.postgres_port, "6432") database = local.database_outputs_by_secret[name].database_name username = local.database_outputs_by_secret[name].user_name password = local.database_outputs_by_secret[name].password "ca.crt" = try(var.constants.postgres_ca, "") }, # Кастомные статические поля try(secret.custom_keys, {}), { for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "") }, # Рандомные поля { for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result } ) : secret.secret_type == "rabbitmq" && local.rabbitmq_outputs_by_secret[name] != null ? merge( local.rabbitmq_credentials_by_secret[name], try(secret.custom_keys, {}), { for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "") }, { for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result } ) : secret.secret_type == "kafka" && local.kafka_outputs_by_secret[name] != null ? merge( local.kafka_credentials_by_secret[name], try(secret.custom_keys, {}), { for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "") }, { for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result } ) : merge( try(secret.custom_keys, {}), { for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "") }, { for key, config in try(secret.random_keys, {}) : key => random_password.secrets["${name}:${key}"].result } ) } # Если для секрета заданы live-значения (adopt) - используем их 1:1, # иначе собираем data из outputs/custom/constant/random как обычно. secrets_data = { for name, secret in local.secrets_map : name => ( contains(keys(local.secrets_data_override), name) ? local.secrets_data_override[name] : local.secrets_data_computed[name] ) } secrets_with_data = { for name, secret in local.secrets_map : name => { name = secret.name namespace = secret.namespace secret_type = secret.secret_type k8s_type = try(local.k8s_secret_type_map[secret.secret_type], "Opaque") labels = try(secret.labels, {}) annotations = try(secret.annotations, {}) ignore_changes = try(secret.ignore_changes, false) data = try(local.secrets_data[name], {}) } } secrets_with_ignore = { for name, secret in local.secrets_with_data : name => secret if secret.ignore_changes } secrets_without_ignore = { for name, secret in local.secrets_with_data : name => secret if !secret.ignore_changes } } resource "random_password" "secrets" { for_each = local.random_keys_flat length = each.value.length special = each.value.special upper = true lower = true numeric = true lifecycle { ignore_changes = all } } resource "kubernetes_secret" "with_ignore" { for_each = local.secrets_with_ignore metadata { name = each.value.name namespace = each.value.namespace labels = try(each.value.labels, {}) annotations = try(each.value.annotations, {}) } type = each.value.k8s_type # Совпадает с импортированным состоянием (провайдер по умолчанию ставит true и # даёт phantom "+ wait_for_service_account_token = true" на adopt-секретах). # Актуально только для secret-ов типа service-account-token; тут все Opaque. wait_for_service_account_token = false data = each.value.data lifecycle { # Игнорируем data, type и metadata чтобы не было phantom updates ignore_changes = [data, type, metadata] } depends_on = [random_password.secrets] } resource "kubernetes_secret" "without_ignore" { for_each = local.secrets_without_ignore metadata { name = each.value.name namespace = each.value.namespace labels = try(each.value.labels, {}) annotations = try(each.value.annotations, {}) } type = each.value.k8s_type # Совпадает с импортированным состоянием (иначе phantom "+ wait_for_service_account_token = true"). wait_for_service_account_token = false data = each.value.data lifecycle { # type и metadata игнорируем чтобы не было phantom updates ignore_changes = [type, metadata] } depends_on = [random_password.secrets] }