125 lines
4.6 KiB
Text
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,
|
|
}
|