++ fail secrets plan on incomplete depends_on instead of writing empty data

This commit is contained in:
Kochetkov S 2026-08-04 13:19:54 +03:00
parent c7e04cb836
commit 12e406acad

View File

@ -247,6 +247,47 @@ locals {
) )
} }
# Явный контракт для типов с обязательным depends_on. Раньше отсутствующее
# или неполное depends_on тихо резолвилось через try(..., "") в несуществующий
# ключ outputs-карты, outputs_by_secret[name] становился null, и секрет молча
# создавался ПУСТЫМ (без host/port/username/password) - без единой ошибки в
# плане. Для type=yc_sa такой защиты не было вообще: null-источник просто не
# проверялся, и итоговые access_key/secret_key/service_account_id молча
# оставались пустыми строками. Здесь это - явная ошибка плана. Секреты с
# готовым статическим data (adopt/1:1) под контракт не подпадают: они не
# резолвят depends_on вообще.
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"
: ""
) :
""
)
}
# Если для секрета заданы явные значения (data) - используем их 1:1, # Если для секрета заданы явные значения (data) - используем их 1:1,
# иначе собираем data из outputs/custom/constant/random как обычно. # иначе собираем data из outputs/custom/constant/random как обычно.
secrets_data = { secrets_data = {
@ -326,6 +367,10 @@ resource "kubernetes_secret" "with_ignore" {
lifecycle { lifecycle {
# Игнорируем data, type и metadata чтобы не было phantom updates # Игнорируем data, type и metadata чтобы не было phantom updates
ignore_changes = [data, type, metadata, wait_for_service_account_token] 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]}"
}
} }
} }
@ -350,6 +395,10 @@ resource "kubernetes_secret" "without_ignore" {
lifecycle { lifecycle {
# type и metadata игнорируем чтобы не было phantom updates # type и metadata игнорируем чтобы не было phantom updates
ignore_changes = [type, metadata, wait_for_service_account_token] 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]}"
}
} }
} }
@ -363,4 +412,11 @@ resource "vault_kv_secret_v2" "this" {
data_json = jsonencode(merge(each.value.data, { data_json = jsonencode(merge(each.value.data, {
for key, config in each.value.random_keys : key => random_password.secrets["${each.key}:${key}"].result 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]}"
}
}
} }