terraform-contour-mirror/modules/k8s-secret/main.tf
2026-08-05 10:42:31 +03:00

399 lines
16 KiB
HCL

locals {
k8s_secret_type_map = {
"dockerconfigjson" = "kubernetes.io/dockerconfigjson"
"database" = "Opaque"
"database_url" = "Opaque"
"s3" = "Opaque"
"valkey" = "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 secret in var.secrets : "${secret.namespace}/${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.depends_on.cluster, "")}:${try(secret.depends_on.db, "")}:${try(secret.depends_on.user, "")}"],
null
)
}
valkey_dependency_keys = {
for name, secret in local.secrets_map : name => {
cluster = try(secret.depends_on.valkey_cluster, "") != "" ? try(secret.depends_on.valkey_cluster, "") : try(secret.depends_on.cluster, "")
user = try(secret.depends_on.valkey_user, "") != "" ? try(secret.depends_on.valkey_user, "") : try(secret.depends_on.user, "")
}
}
valkey_outputs_by_secret = {
for name, secret in local.secrets_map : name => try(
var.valkey_outputs_map["${local.valkey_dependency_keys[name].cluster}:${local.valkey_dependency_keys[name].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.depends_on.rabbitmq_ref, "default"),
try(secret.depends_on.rabbitmq_vhost, try(secret.depends_on.vhost, "")),
try(secret.depends_on.rabbitmq_user, try(secret.depends_on.user, ""))
)
],
null
)
}
kafka_outputs_by_secret = {
for name, secret in local.secrets_map : name => try(
var.kafka_outputs_map[
format(
"%s:%s",
try(coalesce(secret.depends_on.kafka_ref, secret.depends_on.cluster_ref, secret.depends_on.cluster), ""),
try(coalesce(secret.depends_on.kafka_user, secret.depends_on.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.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.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.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.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.depends_on.service_account, "")],
null
)
if 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.type == "yc_sa"
}
secrets_data_override = {
for name, secret in local.secrets_map : name => {
for key, b64 in try(secret.data, {}) : key => base64decode(b64)
}
if length(try(secret.data, {})) > 0
}
secrets_data_computed = {
for name, secret in local.secrets_map : name =>
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.type == "s3" ? {
access_key = try(var.s3_buckets_map[secret.depends_on.bucket].access_key, var.s3_outputs.access_key)
secret_key = try(var.s3_buckets_map[secret.depends_on.bucket].secret_key, var.s3_outputs.secret_key)
bucket = try(var.s3_buckets_map[secret.depends_on.bucket].bucket_name, var.s3_outputs.bucket_name)
endpoint = try(var.constants.s3_endpoint, "https://storage.yandexcloud.net")
} : 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], "")
}
) : secret.type == "database" && local.database_outputs_by_secret[name] != null ? merge(
{
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], "")
}
) : secret.type == "database_url" && local.database_outputs_by_secret[name] != null ? merge(
{
database_url = format(
"postgresql+asyncpg://%s:%s@%s:%s/%s",
local.database_outputs_by_secret[name].user_name,
local.database_outputs_by_secret[name].password,
local.database_outputs_by_secret[name].host,
try(var.constants.postgres_port, "6432"),
local.database_outputs_by_secret[name].database_name
)
},
try(secret.custom_keys, {}),
{
for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "")
}
) : secret.type == "valkey" && local.valkey_outputs_by_secret[name] != null ? merge(
{
cert = try(var.constants.valkey_ca, try(var.constants.postgres_ca, ""))
host = local.valkey_outputs_by_secret[name].host
login = local.valkey_outputs_by_secret[name].user_name
password = local.valkey_outputs_by_secret[name].password
port = tostring(try(local.valkey_outputs_by_secret[name].port, try(var.constants.valkey_port, "6380")))
url = format("%s:%s", local.valkey_outputs_by_secret[name].host, tostring(try(local.valkey_outputs_by_secret[name].port, try(var.constants.valkey_port, "6380"))))
},
try(secret.custom_keys, {}),
{
for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "")
}
) : 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], "")
}
) : 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], "")
}
) : merge(
try(secret.custom_keys, {}),
{
for key, constant_name in try(secret.constant_keys, {}) : key => try(var.constants[constant_name], "")
}
)
}
secrets_contract_errors = {
for name, secret in local.secrets_map : name => (
length(try(secret.data, {})) > 0 ? "" :
secret.type == "database" || secret.type == "database_url" ? (
try(secret.depends_on.cluster, "") == "" || try(secret.depends_on.db, "") == "" || try(secret.depends_on.user, "") == ""
? "type=${secret.type} requires depends_on.cluster, depends_on.db, depends_on.user (got cluster=${try(secret.depends_on.cluster, "")}, db=${try(secret.depends_on.db, "")}, user=${try(secret.depends_on.user, "")})"
: ""
) :
secret.type == "valkey" ? (
local.valkey_dependency_keys[name].cluster == "" || local.valkey_dependency_keys[name].user == ""
? "type=valkey requires depends_on.cluster (or valkey_cluster) and depends_on.user (or valkey_user)"
: ""
) :
secret.type == "rabbitmq" ? (
try(secret.depends_on.rabbitmq_vhost, try(secret.depends_on.vhost, "")) == "" || try(secret.depends_on.rabbitmq_user, try(secret.depends_on.user, "")) == ""
? "type=rabbitmq requires depends_on.vhost (or rabbitmq_vhost) and depends_on.user (or rabbitmq_user)"
: ""
) :
secret.type == "kafka" ? (
try(coalesce(secret.depends_on.kafka_ref, secret.depends_on.cluster_ref, secret.depends_on.cluster), "") == "" || try(coalesce(secret.depends_on.kafka_user, secret.depends_on.user), "") == ""
? "type=kafka requires depends_on.kafka_ref (or cluster_ref/cluster) and depends_on.kafka_user (or user)"
: ""
) :
secret.type == "yc_sa" ? (
try(secret.depends_on.service_account, "") == ""
? "type=yc_sa requires depends_on.service_account"
: ""
) :
secret.type == "dockerconfigjson" && !try(secret.ignore_changes, false) ? (
trimspace(try(var.env_vars.DOCKER_REGISTRY_USERNAME, "")) == "" || trimspace(try(var.env_vars.DOCKER_REGISTRY_PASSWORD, "")) == ""
? "type=dockerconfigjson requires module-level env_vars.DOCKER_REGISTRY_USERNAME and env_vars.DOCKER_REGISTRY_PASSWORD to be non-empty"
: ""
) :
""
)
}
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
type = secret.type
k8s_type = try(local.k8s_secret_type_map[secret.type], "Opaque")
labels = try(secret.labels, {})
annotations = try(secret.annotations, {})
ignore_changes = try(secret.ignore_changes, false)
sink = try(secret.sink, "k8s")
vault_path = try(secret.vault_path, "") != "" ? secret.vault_path : "${var.vault_path_prefix}/${secret.name}"
data = try(local.secrets_data[name], {})
random_keys = contains(keys(local.secrets_data_override), name) ? {} : try(secret.random_keys, {})
}
}
secrets_with_ignore = {
for name, secret in local.secrets_with_data : name => secret
if secret.sink == "k8s" && secret.ignore_changes
}
secrets_without_ignore = {
for name, secret in local.secrets_with_data : name => secret
if secret.sink == "k8s" && !secret.ignore_changes
}
secrets_vault = {
for name, secret in local.secrets_with_data : name => secret
if secret.sink == "vault"
}
}
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
data = merge(each.value.data, {
for key, config in each.value.random_keys : key => random_password.secrets["${each.key}:${key}"].result
})
lifecycle {
ignore_changes = [data, type, metadata, wait_for_service_account_token]
precondition {
condition = local.secrets_contract_errors[each.key] == ""
error_message = "Secret contract violation for ${each.key}: ${local.secrets_contract_errors[each.key]}"
}
}
}
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
data = merge(each.value.data, {
for key, config in each.value.random_keys : key => random_password.secrets["${each.key}:${key}"].result
})
lifecycle {
ignore_changes = [type, metadata, wait_for_service_account_token]
precondition {
condition = local.secrets_contract_errors[each.key] == ""
error_message = "Secret contract violation for ${each.key}: ${local.secrets_contract_errors[each.key]}"
}
}
}
resource "vault_kv_secret_v2" "this" {
for_each = local.secrets_vault
mount = var.vault_kv_mount
name = each.value.vault_path
data_json = jsonencode(merge(each.value.data, {
for key, config in each.value.random_keys : key => random_password.secrets["${each.key}:${key}"].result
}))
lifecycle {
precondition {
condition = local.secrets_contract_errors[each.key] == ""
error_message = "Secret contract violation for ${each.key}: ${local.secrets_contract_errors[each.key]}"
}
}
}