provisioning/schemas/lib/integration/cabling.ncl

94 lines
3.5 KiB
Text
Raw Normal View History

# schemas/lib/integration/cabling.ncl
#
# Cabling format: per-workspace, per-mode binding file that resolves
# domain context fields to their concrete sources.
#
# Location: infra/<ws>/integrations/<mode-id>.ncl
#
# Each entry in `bindings` maps a dotted domain field path
# (e.g. "secret-delivery.registry_password") to a Resolver record.
#
# Resolver kinds (discriminated by the `kind` field):
# "sops" — decrypt field from a SOPS-encrypted file
# "component" — read field from a component's output record
# "literal" — static hardcoded value
# "env" — read from an environment variable at assembly time
#
# Usage:
# "secret-delivery.registry_password" = { kind = "sops", path = "secrets/zot.sops.yaml", key = "ZOT_HTPASSWD" },
# "secret-delivery.registry_url" = { kind = "component", name = "zot", field = "registry_url" },
# "event-emission.subject_prefix" = { kind = "literal", value = "ws.libre-wuji.build.lian-build" },
# "compute.api_key" = { kind = "env", env_var = "HETZNER_API_KEY" },
let _valid_kinds = [| 'sops, 'component, 'literal, 'env |] in
let _Resolver =
std.contract.custom (fun label value =>
if !std.is_record value then
std.contract.blame_with_message "Resolver must be a record with a 'kind' field" label
else if !std.record.has_field "kind" value then
std.contract.blame_with_message "Resolver missing required field 'kind'" label
else
match {
"sops" =>
if std.record.has_field "path" value && std.record.has_field "key" value then
'Ok value
else
std.contract.blame_with_message
"Resolver kind='sops' requires fields: path (String), key (String)"
label,
"component" =>
if std.record.has_field "name" value && std.record.has_field "field" value then
'Ok value
else
std.contract.blame_with_message
"Resolver kind='component' requires fields: name (String), field (String)"
label,
"literal" =>
if std.record.has_field "value" value then
'Ok value
else
std.contract.blame_with_message
"Resolver kind='literal' requires field: value (any)"
label,
"env" =>
if std.record.has_field "env_var" value then
'Ok value
else
std.contract.blame_with_message
"Resolver kind='env' requires field: env_var (String)"
label,
_ =>
std.contract.blame_with_message
"Unknown Resolver kind '%{value.kind}'. Valid: sops, component, literal, env"
label,
} value.kind
) in
# Base shape for structural validation before cross-field checks.
let _CablingBase = {
mode_id | String
| doc "Integration mode id — e.g. 'lian-build-provisioning'",
workspace | String
| doc "Workspace identifier — e.g. 'libre-wuji'",
bindings | { _ | _Resolver }
| doc "Map of '<domain-id>.<field>' to a Resolver",
} in
# Full Cabling contract: structural + non-empty bindings.
let _Cabling =
std.contract.custom (fun label value =>
let validated = value | _CablingBase in
if std.record.length validated.bindings == 0 then
std.contract.blame_with_message
"Cabling '%{validated.mode_id}': bindings must be non-empty"
label
else
'Ok validated
) in
{
Resolver = _Resolver,
Cabling = _Cabling,
}