terraform-contour-mirror/live/secrets/terragrunt.hcl
Kochetkov S 0b7545670f
All checks were successful
Terraform Terragrunt (contour) / validate (push) Successful in 59s
Terraform Terragrunt (contour) / plan (push) Successful in 1m5s
Terraform Terragrunt (contour) / apply (push) Successful in 1m56s
++ service contract compiler, issues on brusnika-stage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:17:33 +03:00

320 lines
14 KiB
HCL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

include "root" {
path = find_in_parent_folders()
}
locals {
repo_root = try(get_repo_root(), "${get_terragrunt_dir()}/../..")
infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml"))
env_override = trimspace(get_env("INFRA_ENV", ""))
env_from_file = try(trimspace(local.infra_config.current_environment), "")
available_envs = sort(keys(try(local.infra_config.environments, {})))
env_fallback = try(local.available_envs[0], "")
env_name = local.env_override != "" ? local.env_override : (
local.env_from_file != "" ? local.env_from_file : local.env_fallback
)
secret_values_env_file = get_env("INFRA_SECRET_VALUES_FILE", "")
secret_values_raw = local.secret_values_env_file != "" ? file(local.secret_values_env_file) : sops_decrypt_file("${local.repo_root}/infrastructure-secrets.yaml")
secret_values_config = yamldecode(local.secret_values_raw)
_fail_on_missing_secret_env = !contains(keys(local.secret_values_config.environments), local.env_name) ? file("INFRA_ENV '${local.env_name}' not found in infrastructure-secrets.yaml environments: [${join(", ", keys(local.secret_values_config.environments))}]") : ""
secret_values_env = local.secret_values_config.environments[local.env_name]
allowed_secret_env_keys = toset(["secrets", "vault"])
secret_env_key_typos = [
for k in keys(local.secret_values_env) : "env '${local.env_name}': unknown top-level key '${k}'"
if !contains(local.allowed_secret_env_keys, k)
]
_fail_on_secret_env_key_typos = length(local.secret_env_key_typos) > 0 ? file("Unknown/misspelled top-level key(s) in infrastructure-secrets.yaml: ${join(" | ", local.secret_env_key_typos)}") : ""
# компилятор контракта; для непереехавших окружений отдаёт пустой список.
# Вливаем ДО разбора v2 - тогда валидация ключей, вывод type из source.kind и
# разворачивание targets ниже работают поверх его записей без единой правки.
shared = read_terragrunt_config("${local.repo_root}/live/_shared/config.hcl")
secrets = concat(try(local.secret_values_env.secrets, []), local.shared.locals.secrets_v2)
# v2 contract detection: a record is v2 if it carries any v2-only key.
# Everything else is legacy v1 and keeps working exactly as before.
v2_marker_keys = ["id", "schema", "source", "targets", "ownership", "extra_fields"]
secrets_v1 = [for secret in local.secrets : secret if length([for k in local.v2_marker_keys : k if contains(keys(secret), k)]) == 0]
secrets_v2 = [for secret in local.secrets : secret if length([for k in local.v2_marker_keys : k if contains(keys(secret), k)]) > 0]
allowed_secret_keys_v1 = toset([
"name", "namespace", "type", "registry_url", "data", "depends_on",
"yc_sa_key_names", "custom_keys", "constant_keys", "random_keys",
"credential_keys", "labels", "annotations", "lifecycle", "sink", "vault_path",
])
secret_key_typos_v1 = flatten([
for secret in local.secrets_v1 : [
for k in keys(secret) : "${try(secret.namespace, "?")}/${try(secret.name, "?")}: unknown key '${k}'"
if !contains(local.allowed_secret_keys_v1, k)
]
])
allowed_secret_keys_v2 = toset([
"id", "schema", "namespace", "ownership", "source", "targets", "extra_fields",
"type", "depends_on", "registry_url", "data", "yc_sa_key_names", "custom_keys",
"constant_keys", "random_keys", "credential_keys", "labels", "annotations", "lifecycle",
])
secret_key_typos_v2 = flatten([
for secret in local.secrets_v2 : [
for k in keys(secret) : "${try(secret.id, "?")}: unknown key '${k}'"
if !contains(local.allowed_secret_keys_v2, k)
]
])
secret_key_typos = concat(local.secret_key_typos_v1, local.secret_key_typos_v2)
_fail_on_secret_key_typos = length(local.secret_key_typos) > 0 ? file("Unknown/misspelled key(s) in infrastructure-secrets.yaml secrets: ${join(" | ", local.secret_key_typos)}") : ""
# v2: required fields present, targets non-empty.
v2_missing_fields = flatten([
for secret in local.secrets_v2 : [
for f in ["id", "schema", "namespace", "targets"] :
"${try(secret.id, "?")}: missing required field '${f}'"
if !contains(keys(secret), f) || (f == "targets" && length(try(secret.targets, [])) == 0)
]
])
_fail_on_v2_missing_fields = length(local.v2_missing_fields) > 0 ? file("v2 secret contract errors: ${join(" | ", local.v2_missing_fields)}") : ""
known_target_kinds = toset(["kubernetes", "vault"])
v2_target_kind_typos = flatten([
for secret in local.secrets_v2 : [
for t in try(secret.targets, []) : "${secret.id}: unknown target.kind '${try(t.kind, "?")}'"
if !contains(local.known_target_kinds, try(t.kind, ""))
]
])
_fail_on_v2_target_kind_typos = length(local.v2_target_kind_typos) > 0 ? file("v2 secret target errors: ${join(" | ", local.v2_target_kind_typos)}") : ""
# v2 source.kind -> legacy type/depends_on translation. An explicit top-level
# "type" on the record always wins (bridge for callers that want to combine
# the new schema/ownership/targets machinery with a hand-picked legacy type).
v2_source_kind = { for secret in local.secrets_v2 : secret.id => try(secret.source.kind, "") }
v2_type_from_source = { for secret in local.secrets_v2 : secret.id => (
try(secret.type, "") != "" ? secret.type :
local.v2_source_kind[secret.id] == "postgresql_user" ? "database" :
local.v2_source_kind[secret.id] == "kafka_user" ? "kafka" :
local.v2_source_kind[secret.id] == "rabbitmq_user" ? "rabbitmq" :
local.v2_source_kind[secret.id] == "s3_bucket" ? "s3" :
local.v2_source_kind[secret.id] == "valkey_user" ? "valkey" :
local.v2_source_kind[secret.id] == "secret_ref" ? (try(secret.schema, "") == "dockerconfigjson.v1" ? "dockerconfigjson" : "opaque") :
"opaque"
) }
v2_depends_on_from_source = { for secret in local.secrets_v2 : secret.id => (
try(secret.depends_on, null) != null ? secret.depends_on :
local.v2_source_kind[secret.id] == "postgresql_user" ? {
cluster = try(secret.source.ref.cluster, "")
db = try(secret.source.ref.database, "")
user = try(secret.source.ref.user, "")
} :
local.v2_source_kind[secret.id] == "kafka_user" ? {
kafka_ref = try(secret.source.ref.cluster, "")
kafka_user = try(secret.source.ref.user, "")
} :
local.v2_source_kind[secret.id] == "rabbitmq_user" ? {
rabbitmq_ref = try(secret.source.ref.cluster, "default")
rabbitmq_vhost = try(secret.source.ref.vhost, "")
rabbitmq_user = try(secret.source.ref.user, "")
} :
local.v2_source_kind[secret.id] == "s3_bucket" ? {
bucket = try(secret.source.ref.bucket, "")
} :
local.v2_source_kind[secret.id] == "valkey_user" ? {
valkey_cluster = try(secret.source.ref.cluster, "")
valkey_user = try(secret.source.ref.user, "")
} :
{}
) }
# secret_ref's ref is a plain "vault://path#field" string (unlike the other
# source kinds, whose ref is an object) - only meaningful for source_kind=secret_ref.
# source.ref is a string for secret_ref but an object for every other
# source.kind (postgresql_user/kafka_user/rabbitmq_user/s3_bucket/valkey_user)
# - HCL's conditional requires both branches to share a static type, so the
# object branch is forced through tostring()/try() to collapse to "".
v2_source_ref_string = { for secret in local.secrets_v2 : secret.id =>
local.v2_source_kind[secret.id] == "secret_ref" ? try(tostring(secret.source.ref), "") : ""
}
v2_extra_fields = { for secret in local.secrets_v2 : secret.id => {
for key, ef in try(secret.extra_fields, {}) : key => {
source_kind = try(ef.source.kind, "")
ref = try(ef.source.ref, "")
value = try(ef.source.value, "")
}
} }
# One flattened entry per target: same shape as legacy secrets_input, plus
# the v2 fields the module understands (schema/ownership/source_kind/source_ref/extra_fields).
secrets_v2_flat = flatten([
for secret in local.secrets_v2 : [
for t in secret.targets : {
name = try(t.name, secret.id)
namespace = try(t.namespace, try(t.kind, "") == "vault" ? "" : secret.namespace)
type = local.v2_type_from_source[secret.id]
registry_url = try(secret.registry_url, "")
data = try(secret.data, {})
depends_on = local.v2_depends_on_from_source[secret.id]
yc_sa_key_names = try(secret.yc_sa_key_names, {})
custom_keys = try(secret.custom_keys, {})
constant_keys = try(secret.constant_keys, {})
random_keys = try(secret.random_keys, {})
credential_keys = try(secret.credential_keys, {})
labels = try(secret.labels, {})
annotations = try(secret.annotations, {})
ignore_changes = try(secret.lifecycle.ignore_changes, false)
sink = try(t.kind, "") == "vault" ? "vault" : "k8s"
vault_path = try(t.kind, "") == "vault" ? try(t.path, secret.id) : ""
schema = try(secret.schema, "")
ownership = try(secret.ownership, "managed")
source_kind = local.v2_source_kind[secret.id]
source_ref = local.v2_source_ref_string[secret.id]
extra_fields = local.v2_extra_fields[secret.id]
}
]
])
regcred_dockerconfigjson = try(local.secret_values_env.vault.data.common.regcred.dockerconfigjson, "")
regcred_valid = can(jsondecode(local.regcred_dockerconfigjson))
regcred_namespaces = [
for ns in try(local.infra_config.environments[local.env_name].namespaces, []) :
ns.name if try(ns.image_pull_secret, false)
]
regcred_secrets = local.regcred_valid ? [
for ns in local.regcred_namespaces : {
name = "regcred"
namespace = ns
type = "dockerconfigjson"
registry_url = ""
data = { ".dockerconfigjson" = base64encode(local.regcred_dockerconfigjson) }
depends_on = {}
yc_sa_key_names = {}
custom_keys = {}
constant_keys = {}
random_keys = {}
credential_keys = {}
labels = {}
annotations = {}
ignore_changes = false
sink = "k8s"
vault_path = ""
}
] : []
secrets_input = [
for secret in local.secrets_v1 : {
name = secret.name
namespace = try(secret.namespace, "")
type = try(secret.type, "opaque")
registry_url = try(secret.registry_url, "")
data = try(secret.data, {})
depends_on = try(secret.depends_on, {})
yc_sa_key_names = try(secret.yc_sa_key_names, {})
custom_keys = try(secret.custom_keys, {})
constant_keys = try(secret.constant_keys, {})
random_keys = try(secret.random_keys, {})
credential_keys = try(secret.credential_keys, {})
labels = try(secret.labels, {})
annotations = try(secret.annotations, {})
ignore_changes = try(secret.lifecycle.ignore_changes, false)
sink = try(secret.sink, "k8s")
vault_path = try(secret.vault_path, "")
}
]
}
dependency "namespace" {
config_path = "../namespace"
mock_outputs = { namespaces = {}, names = [] }
mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"]
}
dependency "s3" {
config_path = "../s3"
mock_outputs = { bucket_name = "mock", access_key = "mock", secret_key = "mock", buckets = {}, service_accounts = {} }
mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"]
}
dependency "database" {
config_path = "../database"
mock_outputs = { database_outputs_map = {} }
mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"]
}
dependency "rabbitmq" {
config_path = "../rabbitmq"
mock_outputs = { rabbitmq_outputs_map = {} }
mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"]
}
dependency "kafka_topics" {
config_path = "../kafka-topics"
mock_outputs = { kafka_outputs_map = {} }
mock_outputs_allowed_terraform_commands = ["init", "validate", "plan"]
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
endpoint = get_env("TF_STATE_S3_ENDPOINT", "")
bucket = get_env("TF_STATE_S3_BUCKET", "")
key = "${local.env_name}/secrets/terraform.tfstate"
region = get_env("TF_STATE_S3_REGION", "ru-central1")
access_key = get_env("S3_ACCESS_KEY", get_env("AWS_ACCESS_KEY_ID", ""))
secret_key = get_env("S3_SECRET_KEY", get_env("AWS_SECRET_ACCESS_KEY", ""))
skip_region_validation = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_bucket_root_access = true
force_path_style = true
}
}
terraform {
source = "${get_terragrunt_dir()}/../../modules//k8s-secret"
}
inputs = {
secrets = concat(local.secrets_input, local.regcred_secrets, local.secrets_v2_flat)
s3_outputs = {
bucket_name = dependency.s3.outputs.bucket_name
access_key = dependency.s3.outputs.access_key
secret_key = dependency.s3.outputs.secret_key
}
s3_buckets_map = try(dependency.s3.outputs.buckets, {})
yc_service_accounts_map = try(dependency.s3.outputs.service_accounts, {})
database_outputs_map = dependency.database.outputs.database_outputs_map
rabbitmq_outputs_map = try(dependency.rabbitmq.outputs.rabbitmq_outputs_map, {})
kafka_outputs_map = try(dependency.kafka_topics.outputs.kafka_outputs_map, {})
valkey_outputs_map = {}
constants = {
s3_endpoint = try(local.infra_config.environments[local.env_name].minio.endpoint, "http://minio.minio.svc.cluster.local:9000")
postgres_port = "5432"
postgres_ca = ""
valkey_port = "6379"
valkey_ca = ""
}
vault_kv_mount = try(local.secret_values_env.vault.kv_mount, "secrets")
vault_path_prefix = try(local.secret_values_env.vault.path_prefix, "vault")
env_vars = {
DOCKER_REGISTRY_USERNAME = get_env("DOCKER_REGISTRY_USERNAME", "")
DOCKER_REGISTRY_PASSWORD = get_env("DOCKER_REGISTRY_PASSWORD", "")
}
kubeconfig_path = get_env("KUBECONFIG", "")
kube_context = get_env("KUBE_CONTEXT", "")
}