mirror of
https://gitlab.sarex.io/infra/terraform-contour-mirror.git
synced 2026-08-05 18:31:00 +03:00
module: optional schema/ownership/source_kind/source_ref/extra_fields fields, backward compatible - legacy secrets keep resolving exactly as before. ownership modes managed/referenced/observed/adopt_once replace ignore_changes for v2 records; referenced reads a value straight from vault via a data source; observed only verifies presence, writes nothing. terragrunt: v1/v2 detection by marker keys, targets fan-out into the existing flat secrets list (one entry per target, same resource-key scheme as today), source.kind -> type/depends_on translation for the five documented kinds. brusnika-stage: acceptance environment for the two test cases from the plan - regcred (dockerconfigjson.v1, ownership=referenced, value from vault) and a rabbitmq.v1 secret (ownership=managed) fanned out to both kubernetes and vault targets from a single declaration.
312 lines
14 KiB
HCL
312 lines
14 KiB
HCL
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.
|
|
v2_source_ref_string = { for secret in local.secrets_v2 : secret.id =>
|
|
local.v2_source_kind[secret.id] == "secret_ref" ? try(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", "")
|
|
}
|