mirror of
https://gitlab.sarex.io/infra/terraform-contour-mirror.git
synced 2026-08-05 18:31:00 +03:00
++ add declarative secrets contract v2 (schema/ownership/targets/extra_fields)
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.
This commit is contained in:
parent
8b98cee0a5
commit
d645e43de5
File diff suppressed because it is too large
Load Diff
@ -533,3 +533,34 @@ environments:
|
|||||||
vhost: test-vhost
|
vhost: test-vhost
|
||||||
type: topic
|
type: topic
|
||||||
durable: true
|
durable: true
|
||||||
|
|
||||||
|
# brusnika-stage — приёмочный тест декларативного контракта секретов v2
|
||||||
|
# (см. SECRETS-CONTRACT-PLAN.md) перед массовым переключением на схемы.
|
||||||
|
brusnika-stage:
|
||||||
|
namespaces:
|
||||||
|
- name: test-terraform
|
||||||
|
manage: true
|
||||||
|
image_pull_secret: false # regcred сюда приходит через v2 ownership=referenced, не через legacy fan-out
|
||||||
|
|
||||||
|
# In-cluster RabbitMQ (bitnami, ns django). Admin creds - secrets/rabbitmq/auth
|
||||||
|
# в Vault, засеяно вручную из существующего django/rabbitmq-secret (см. план,
|
||||||
|
# раздел 11 "Предусловие").
|
||||||
|
rabbitmq:
|
||||||
|
amqp_host: rabbitmq-service.django.svc.cluster.local
|
||||||
|
amqp_port: 5672
|
||||||
|
vhosts:
|
||||||
|
- name: test-terraform
|
||||||
|
users:
|
||||||
|
- name: test-terraform
|
||||||
|
password_length: 32
|
||||||
|
password_special: false
|
||||||
|
permissions:
|
||||||
|
- user: test-terraform
|
||||||
|
vhost: test-terraform
|
||||||
|
configure: ".*"
|
||||||
|
write: ".*"
|
||||||
|
read: ".*"
|
||||||
|
queues:
|
||||||
|
- name: test-terraform-probe
|
||||||
|
vhost: test-terraform
|
||||||
|
durable: true
|
||||||
|
|||||||
@ -32,19 +32,144 @@ locals {
|
|||||||
|
|
||||||
secrets = try(local.secret_values_env.secrets, [])
|
secrets = try(local.secret_values_env.secrets, [])
|
||||||
|
|
||||||
allowed_secret_keys = toset([
|
# 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",
|
"name", "namespace", "type", "registry_url", "data", "depends_on",
|
||||||
"yc_sa_key_names", "custom_keys", "constant_keys", "random_keys",
|
"yc_sa_key_names", "custom_keys", "constant_keys", "random_keys",
|
||||||
"credential_keys", "labels", "annotations", "lifecycle", "sink", "vault_path",
|
"credential_keys", "labels", "annotations", "lifecycle", "sink", "vault_path",
|
||||||
])
|
])
|
||||||
secret_key_typos = flatten([
|
secret_key_typos_v1 = flatten([
|
||||||
for secret in local.secrets : [
|
for secret in local.secrets_v1 : [
|
||||||
for k in keys(secret) : "${try(secret.namespace, "?")}/${try(secret.name, "?")}: unknown key '${k}'"
|
for k in keys(secret) : "${try(secret.namespace, "?")}/${try(secret.name, "?")}: unknown key '${k}'"
|
||||||
if !contains(local.allowed_secret_keys, 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)}") : ""
|
_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_dockerconfigjson = try(local.secret_values_env.vault.data.common.regcred.dockerconfigjson, "")
|
||||||
regcred_valid = can(jsondecode(local.regcred_dockerconfigjson))
|
regcred_valid = can(jsondecode(local.regcred_dockerconfigjson))
|
||||||
regcred_namespaces = [
|
regcred_namespaces = [
|
||||||
@ -73,7 +198,7 @@ locals {
|
|||||||
] : []
|
] : []
|
||||||
|
|
||||||
secrets_input = [
|
secrets_input = [
|
||||||
for secret in local.secrets : {
|
for secret in local.secrets_v1 : {
|
||||||
name = secret.name
|
name = secret.name
|
||||||
namespace = try(secret.namespace, "")
|
namespace = try(secret.namespace, "")
|
||||||
type = try(secret.type, "opaque")
|
type = try(secret.type, "opaque")
|
||||||
@ -88,8 +213,8 @@ locals {
|
|||||||
labels = try(secret.labels, {})
|
labels = try(secret.labels, {})
|
||||||
annotations = try(secret.annotations, {})
|
annotations = try(secret.annotations, {})
|
||||||
ignore_changes = try(secret.lifecycle.ignore_changes, false)
|
ignore_changes = try(secret.lifecycle.ignore_changes, false)
|
||||||
sink = try(secret.sink, "k8s")
|
sink = try(secret.sink, "k8s")
|
||||||
vault_path = try(secret.vault_path, "")
|
vault_path = try(secret.vault_path, "")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -150,7 +275,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
secrets = concat(local.secrets_input, local.regcred_secrets)
|
secrets = concat(local.secrets_input, local.regcred_secrets, local.secrets_v2_flat)
|
||||||
|
|
||||||
s3_outputs = {
|
s3_outputs = {
|
||||||
bucket_name = dependency.s3.outputs.bucket_name
|
bucket_name = dependency.s3.outputs.bucket_name
|
||||||
|
|||||||
@ -240,9 +240,108 @@ locals {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
known_ownerships = ["", "managed", "referenced", "observed", "adopt_once"]
|
||||||
|
known_schemas = [
|
||||||
|
"", "postgresql.v1", "kafka.v1", "rabbitmq.v1", "s3.v1", "valkey.v1",
|
||||||
|
"dockerconfigjson.v1", "opaque.v1",
|
||||||
|
]
|
||||||
|
|
||||||
|
# extra_fields flattened to "secret_name::field_key" => {source_kind, ref, value}
|
||||||
|
extra_fields_flat = merge([
|
||||||
|
for name, secret in local.secrets_map : {
|
||||||
|
for key, ef in try(secret.extra_fields, {}) : "${name}::${key}" => {
|
||||||
|
source_kind = ef.source_kind
|
||||||
|
ref = try(ef.ref, "")
|
||||||
|
value = try(ef.value, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]...)
|
||||||
|
|
||||||
|
# Every vault_kv_secret_v2 read this module needs to perform for referenced
|
||||||
|
# ownership and extra_fields secret_ref, keyed uniformly so a single
|
||||||
|
# data-source for_each can serve both. ref format: "vault://<kv-name>#<field>".
|
||||||
|
vault_secret_ref_targets = merge(
|
||||||
|
{
|
||||||
|
for name, secret in local.secrets_map : "secret:${name}" => {
|
||||||
|
path = trimprefix(split("#", secret.source_ref)[0], "vault://")
|
||||||
|
key = length(split("#", secret.source_ref)) > 1 ? split("#", secret.source_ref)[1] : ""
|
||||||
|
}
|
||||||
|
if try(secret.ownership, "") == "referenced" && try(secret.source_kind, "") == "secret_ref" && try(secret.source_ref, "") != ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
for k, ef in local.extra_fields_flat : "extra:${k}" => {
|
||||||
|
path = trimprefix(split("#", ef.ref)[0], "vault://")
|
||||||
|
key = length(split("#", ef.ref)) > 1 ? split("#", ef.ref)[1] : ""
|
||||||
|
}
|
||||||
|
if ef.source_kind == "secret_ref" && ef.ref != ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Resolved data for ownership=referenced secrets. dockerconfigjson.v1 is a
|
||||||
|
# single opaque JSON blob under vault_secret_ref_targets[...].key - it lands
|
||||||
|
# in the ".dockerconfigjson" field verbatim. Anything else with a key reads
|
||||||
|
# that one field; without a key, the whole KV object is copied through.
|
||||||
|
secrets_data_referenced = {
|
||||||
|
for name, secret in local.secrets_map : name => (
|
||||||
|
try(secret.schema, "") == "dockerconfigjson.v1"
|
||||||
|
? { ".dockerconfigjson" = try(data.vault_kv_secret_v2.referenced["secret:${name}"].data[local.vault_secret_ref_targets["secret:${name}"].key], "") }
|
||||||
|
: (
|
||||||
|
local.vault_secret_ref_targets["secret:${name}"].key != ""
|
||||||
|
? { (local.vault_secret_ref_targets["secret:${name}"].key) = try(data.vault_kv_secret_v2.referenced["secret:${name}"].data[local.vault_secret_ref_targets["secret:${name}"].key], "") }
|
||||||
|
: { for k, v in try(data.vault_kv_secret_v2.referenced["secret:${name}"].data, {}) : k => tostring(v) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if try(secret.ownership, "") == "referenced" && try(secret.source_kind, "") == "secret_ref" && try(secret.source_ref, "") != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
extra_fields_resolved = {
|
||||||
|
for name, secret in local.secrets_map : name => {
|
||||||
|
for key, ef in try(secret.extra_fields, {}) : key => (
|
||||||
|
ef.source_kind == "secret_ref" && ef.ref != ""
|
||||||
|
? try(data.vault_kv_secret_v2.referenced["extra:${name}::${key}"].data[local.vault_secret_ref_targets["extra:${name}::${key}"].key], "")
|
||||||
|
: try(ef.value, "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Base data before schema field-naming and extra_fields: referenced value
|
||||||
|
# if this secret is ownership=referenced, otherwise the existing
|
||||||
|
# type-driven computation (unchanged for every legacy secret).
|
||||||
|
secrets_data_base = {
|
||||||
|
for name, secret in local.secrets_map : name => (
|
||||||
|
contains(keys(local.secrets_data_referenced), name)
|
||||||
|
? local.secrets_data_referenced[name]
|
||||||
|
: local.secrets_data_computed[name]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# rabbitmq.v1 differs from the legacy field-set only in "hostname": legacy
|
||||||
|
# ships host:port there, the canonical schema wants host alone.
|
||||||
|
secrets_data_schema_adjusted = {
|
||||||
|
for name, secret in local.secrets_map : name => (
|
||||||
|
try(secret.schema, "") == "rabbitmq.v1" && local.rabbitmq_outputs_by_secret[name] != null
|
||||||
|
? merge(local.secrets_data_base[name], { hostname = local.rabbitmq_outputs_by_secret[name].host })
|
||||||
|
: local.secrets_data_base[name]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
secrets_contract_errors = {
|
secrets_contract_errors = {
|
||||||
for name, secret in local.secrets_map : name => (
|
for name, secret in local.secrets_map : name => (
|
||||||
length(try(secret.data, {})) > 0 ? "" :
|
length(try(secret.data, {})) > 0 ? "" :
|
||||||
|
!contains(local.known_ownerships, try(secret.ownership, "")) ?
|
||||||
|
"unknown ownership '${try(secret.ownership, "")}' (expected one of: ${join(", ", local.known_ownerships)})" :
|
||||||
|
!contains(local.known_schemas, try(secret.schema, "")) ?
|
||||||
|
"unknown schema '${try(secret.schema, "")}' (expected one of: ${join(", ", local.known_schemas)})" :
|
||||||
|
length([for k, ef in try(secret.extra_fields, {}) : k if !contains(["secret_ref", "static"], ef.source_kind)]) > 0 ?
|
||||||
|
"extra_fields has unknown source_kind (expected secret_ref|static)" :
|
||||||
|
length([for k, ef in try(secret.extra_fields, {}) : k if ef.source_kind == "secret_ref" && try(ef.ref, "") == ""]) > 0 ?
|
||||||
|
"extra_fields secret_ref entries require non-empty ref (vault://<path>#<field>)" :
|
||||||
|
try(secret.ownership, "") == "observed" ? "" :
|
||||||
|
try(secret.ownership, "") == "referenced" ? (
|
||||||
|
try(secret.source_kind, "") != "secret_ref" ? "ownership=referenced requires source_kind=secret_ref" :
|
||||||
|
!can(regex("^vault://.+", try(secret.source_ref, ""))) ? "ownership=referenced requires source_ref matching vault://<path>#<field>" :
|
||||||
|
""
|
||||||
|
) :
|
||||||
secret.type == "database" || secret.type == "database_url" ? (
|
secret.type == "database" || secret.type == "database_url" ? (
|
||||||
try(secret.depends_on.cluster, "") == "" || try(secret.depends_on.db, "") == "" || try(secret.depends_on.user, "") == ""
|
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, "")})"
|
? "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, "")})"
|
||||||
@ -278,10 +377,22 @@ locals {
|
|||||||
}
|
}
|
||||||
|
|
||||||
secrets_data = {
|
secrets_data = {
|
||||||
for name, secret in local.secrets_map : name => (
|
for name, secret in local.secrets_map : name => merge(
|
||||||
contains(keys(local.secrets_data_override), name)
|
contains(keys(local.secrets_data_override), name)
|
||||||
? local.secrets_data_override[name]
|
? local.secrets_data_override[name]
|
||||||
: local.secrets_data_computed[name]
|
: local.secrets_data_schema_adjusted[name],
|
||||||
|
try(local.extra_fields_resolved[name], {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ownership supersedes legacy ignore_changes when set: adopt_once behaves
|
||||||
|
# like the old ignore_changes=true, managed/referenced behave like false.
|
||||||
|
# observed doesn't create a write resource at all (see secrets_observed_*).
|
||||||
|
secrets_ignore_changes = {
|
||||||
|
for name, secret in local.secrets_map : name => (
|
||||||
|
try(secret.ownership, "") == "adopt_once" ? true :
|
||||||
|
try(secret.ownership, "") == "" ? try(secret.ignore_changes, false) :
|
||||||
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +404,8 @@ locals {
|
|||||||
k8s_type = try(local.k8s_secret_type_map[secret.type], "Opaque")
|
k8s_type = try(local.k8s_secret_type_map[secret.type], "Opaque")
|
||||||
labels = try(secret.labels, {})
|
labels = try(secret.labels, {})
|
||||||
annotations = try(secret.annotations, {})
|
annotations = try(secret.annotations, {})
|
||||||
ignore_changes = try(secret.ignore_changes, false)
|
ignore_changes = local.secrets_ignore_changes[name]
|
||||||
|
ownership = try(secret.ownership, "")
|
||||||
sink = try(secret.sink, "k8s")
|
sink = try(secret.sink, "k8s")
|
||||||
vault_path = try(secret.vault_path, "") != "" ? secret.vault_path : "${var.vault_path_prefix}/${secret.name}"
|
vault_path = try(secret.vault_path, "") != "" ? secret.vault_path : "${var.vault_path_prefix}/${secret.name}"
|
||||||
data = try(local.secrets_data[name], {})
|
data = try(local.secrets_data[name], {})
|
||||||
@ -303,17 +415,49 @@ locals {
|
|||||||
|
|
||||||
secrets_with_ignore = {
|
secrets_with_ignore = {
|
||||||
for name, secret in local.secrets_with_data : name => secret
|
for name, secret in local.secrets_with_data : name => secret
|
||||||
if secret.sink == "k8s" && secret.ignore_changes
|
if secret.sink == "k8s" && secret.ignore_changes && secret.ownership != "observed"
|
||||||
}
|
}
|
||||||
secrets_without_ignore = {
|
secrets_without_ignore = {
|
||||||
for name, secret in local.secrets_with_data : name => secret
|
for name, secret in local.secrets_with_data : name => secret
|
||||||
if secret.sink == "k8s" && !secret.ignore_changes
|
if secret.sink == "k8s" && !secret.ignore_changes && secret.ownership != "observed"
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets_vault = {
|
secrets_vault = {
|
||||||
for name, secret in local.secrets_with_data : name => secret
|
for name, secret in local.secrets_with_data : name => secret
|
||||||
if secret.sink == "vault"
|
if secret.sink == "vault" && secret.ownership != "observed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secrets_observed_k8s = {
|
||||||
|
for name, secret in local.secrets_with_data : name => secret
|
||||||
|
if secret.sink == "k8s" && secret.ownership == "observed"
|
||||||
|
}
|
||||||
|
secrets_observed_vault = {
|
||||||
|
for name, secret in local.secrets_with_data : name => secret
|
||||||
|
if secret.sink == "vault" && secret.ownership == "observed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "vault_kv_secret_v2" "referenced" {
|
||||||
|
for_each = local.vault_secret_ref_targets
|
||||||
|
|
||||||
|
mount = var.vault_kv_mount
|
||||||
|
name = each.value.path
|
||||||
|
}
|
||||||
|
|
||||||
|
data "kubernetes_secret" "observed" {
|
||||||
|
for_each = local.secrets_observed_k8s
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
name = each.value.name
|
||||||
|
namespace = each.value.namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "vault_kv_secret_v2" "observed" {
|
||||||
|
for_each = local.secrets_observed_vault
|
||||||
|
|
||||||
|
mount = var.vault_kv_mount
|
||||||
|
name = each.value.vault_path
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "random_password" "secrets" {
|
resource "random_password" "secrets" {
|
||||||
|
|||||||
@ -43,6 +43,27 @@ variable "secrets" {
|
|||||||
# resolution (static/dynamic from module outputs) are identical either way.
|
# resolution (static/dynamic from module outputs) are identical either way.
|
||||||
sink = optional(string, "k8s")
|
sink = optional(string, "k8s")
|
||||||
vault_path = optional(string, "") # KV name relative to mount; default <vault_path_prefix>/<name>
|
vault_path = optional(string, "") # KV name relative to mount; default <vault_path_prefix>/<name>
|
||||||
|
|
||||||
|
# v2 contract fields. All optional - legacy callers (type + depends_on)
|
||||||
|
# keep working unchanged when these are left unset.
|
||||||
|
schema = optional(string, "") # canonical field-set selector, e.g. "rabbitmq.v1", "dockerconfigjson.v1"
|
||||||
|
|
||||||
|
# Ownership mode: managed | referenced | observed | adopt_once. Empty ("")
|
||||||
|
# falls back to legacy behavior derived from ignore_changes.
|
||||||
|
ownership = optional(string, "")
|
||||||
|
|
||||||
|
# Used only when ownership = "referenced": value is read from an external
|
||||||
|
# store instead of computed from module outputs.
|
||||||
|
source_kind = optional(string, "") # currently only "secret_ref" is implemented
|
||||||
|
source_ref = optional(string, "") # "vault://<kv-name-relative-to-mount>#<field>"
|
||||||
|
|
||||||
|
# Extra named fields merged on top of the resolved/canonical data, each
|
||||||
|
# either read from Vault (secret_ref) or a literal non-secret value (static).
|
||||||
|
extra_fields = optional(map(object({
|
||||||
|
source_kind = string # "secret_ref" | "static"
|
||||||
|
ref = optional(string, "") # for secret_ref: "vault://<kv-name>#<field>"
|
||||||
|
value = optional(string, "") # for static
|
||||||
|
})), {})
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user