Skip to main content

cfgd_core/composition/
engine.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use crate::config::{EnvVar, ProfileLayer, ResolvedProfile, validate_secret_specs};
5use crate::errors::Result;
6
7use super::layers::build_source_layers;
8use super::merge::merge_with_policy;
9use super::{CompositionInput, CompositionResult, ConflictResolution};
10
11/// Compose multiple source configs with a local resolved profile.
12/// Local config is always priority 1000. Sources are merged according to policy tiers.
13///
14/// The composition algorithm:
15/// 1. Start with local resolved profile
16/// 2. For each source (sorted by priority ascending):
17///    - Apply locked items unconditionally
18///    - Apply required items (union for packages, source wins for files/env)
19///    - Apply recommended items if accept_recommended && not rejected
20///    - Apply optional items only if opted in
21/// 3. Apply subscriber overrides on top
22/// 4. Validate security constraints
23pub fn compose(local: &ResolvedProfile, sources: &[CompositionInput]) -> Result<CompositionResult> {
24    let mut all_layers: Vec<ProfileLayer> = local.layers.clone();
25    let mut conflicts: Vec<ConflictResolution> = Vec::new();
26    let mut source_env: HashMap<String, Vec<EnvVar>> = HashMap::new();
27
28    // Sort sources by priority ascending (lower priority processed first, higher wins)
29    let mut sorted_sources: Vec<&CompositionInput> = sources.iter().collect();
30    sorted_sources.sort_by(|a, b| {
31        a.priority
32            .cmp(&b.priority)
33            .then(a.source_name.cmp(&b.source_name))
34    });
35
36    for input in &sorted_sources {
37        // Collect source-specific env for template sandboxing
38        let mut env: Vec<EnvVar> = Vec::new();
39        for layer in &input.layers {
40            crate::merge_env(&mut env, &layer.spec.env);
41        }
42        source_env.insert(input.source_name.clone(), env);
43
44        let source_layers = build_source_layers(input, &mut conflicts)?;
45        all_layers.extend(source_layers);
46    }
47
48    // Sort all layers by priority, then merge
49    all_layers.sort_by_key(|a| a.priority);
50
51    let mut merged = merge_with_policy(&all_layers, &mut conflicts)?;
52
53    // Tag files with their source origin for template sandboxing.
54    // Build a HashMap<target, source> respecting layer priority order (higher priority wins).
55    let mut file_origins: HashMap<PathBuf, String> = HashMap::new();
56    for layer in &all_layers {
57        if layer.source != "local"
58            && let Some(ref files) = layer.spec.files
59        {
60            for managed in &files.managed {
61                // Later (higher-priority) layers overwrite earlier entries
62                file_origins.insert(managed.target.clone(), layer.source.clone());
63            }
64        }
65    }
66    for merged_file in &mut merged.files.managed {
67        if merged_file.origin.is_none()
68            && let Some(source) = file_origins.get(&merged_file.target)
69        {
70            merged_file.origin = Some(source.clone());
71        }
72    }
73
74    // Validate secrets from all sources (catches invalid specs from ConfigSources)
75    validate_secret_specs(&merged.secrets)?;
76
77    Ok(CompositionResult {
78        resolved: ResolvedProfile {
79            layers: all_layers,
80            merged,
81        },
82        conflicts,
83        source_env,
84        source_commits: HashMap::new(),
85    })
86}