provisioning/schemas/security/config-merger.ncl

125 lines
4.6 KiB
Text

# Config Merger - Combine defaults, infrastructure config, and secrets
let helpers = import "schemas/platform/common/helpers.ncl" in
{
# Merge configuration with overrides using deep merge
# Preserves nested structures, doesn't lose partial updates
merge_deep = fun base override =>
helpers.compose_config base override base,
# Configuration pipeline: defaults → infrastructure → secrets
pipeline = fun defaults infrastructure_config secrets =>
let step1 = compose_config defaults infrastructure_config {} in
compose_config step1 secrets {},
# Compose configurations with proper precedence
# Order: base (lowest) → layer1 → layer2 (highest)
compose_config = fun base layer1 layer2 =>
let merge_two = fun a b =>
std.record.fold (
fun acc key value =>
if std.record.has_field key a && std.record.is_record (std.record.get key a) && std.record.is_record value then
acc & {
(key) = merge_two (std.record.get key a) value,
}
else
acc & {
(key) = value,
}
) a b in
let combined = merge_two base layer1 in
merge_two combined layer2,
# Extract environment-specific configuration
by_environment = fun config environment =>
let base_config = config | std.record.get_path ["environments", "all"] in
let env_config = config | std.record.get_path ["environments", environment] in
compose_config base_config env_config {},
# Build final deployment configuration
# Inputs: defaults, user overrides, secrets, deployment mode
build = fun defaults user_config secrets deployment_mode environment =>
let base = compose_config defaults user_config {} in
let with_secrets = compose_config base secrets {} in
let mode_config = (
with_secrets | std.record.get_path ["deployment_modes", deployment_mode] | default {}
) in
let final = compose_config with_secrets mode_config {} in
{
config = final,
environment = environment,
deployment_mode = deployment_mode,
merged_from = ["defaults", "user_config", "secrets", "mode_specific"],
},
# Validate merged configuration completeness
validate_complete = fun config required_paths =>
let check_path = fun path =>
let try_get = std.record.get_path (std.string.split "." path) config in
if try_get == null || (std.string.is_string try_get && std.string.is_empty try_get) then
{ path = path, valid = false }
else
{ path = path, valid = true } in
let results = std.array.map check_path required_paths in
let invalid = std.array.filter (fun r => not r.valid) results in
{
valid = std.array.length invalid == 0,
checked_paths = std.array.length required_paths,
invalid_paths = invalid,
},
# Extract configuration for specific component
# Useful for creating component-specific config files
extract_component = fun config component_name =>
let component = std.record.get component_name config in
if std.record.is_record component then
component
else
{},
# Flatten nested configuration for environment variables
flatten = fun config prefix =>
let flatten_impl = fun obj current_prefix =>
std.record.fold (
fun acc key value =>
let new_key = $"($current_prefix)_($key)" in
if std.record.is_record value && not (std.string.is_string value) then
acc & (flatten_impl value new_key)
else
acc & {
(new_key) = if std.string.is_string value then value else std.string.from_number value,
}
) {} obj in
flatten_impl config prefix,
# Create configuration snapshot for audit/compliance
snapshot = fun config metadata =>
{
timestamp = metadata.timestamp,
environment = metadata.environment,
version = metadata.config_version,
deployment_mode = metadata.deployment_mode,
config_hash = (
config | std.json.stringify | std.crypto.sha256
),
merged_sources = metadata.sources,
audit_log = metadata.audit_log,
},
# Deep merge utility - recursive merge of nested records
deep_merge = fun records =>
let merge_pair = fun a b =>
std.record.fold (
fun acc key value =>
if std.record.has_field key a then
let existing = std.record.get key a in
if std.record.is_record existing && std.record.is_record value then
acc & { (key) = deep_merge [existing, value] }
else
acc & { (key) = value }
else
acc & { (key) = value }
) a b in
std.array.fold_left merge_pair {} records,
}