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)}") : "" secrets = try(local.secret_values_env.secrets, []) # 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", "") }