cfgd_core/util/
reconcile.rs1use crate::config;
2
3#[derive(Debug, Clone, serde::Serialize)]
5pub struct EffectiveReconcile {
6 pub interval: String,
7 pub auto_apply: bool,
8 pub drift_policy: config::DriftPolicy,
9}
10
11pub fn resolve_effective_reconcile(
21 module_name: &str,
22 profile_chain: &[&str],
23 reconcile: &config::ReconcileConfig,
24) -> EffectiveReconcile {
25 let mut effective = EffectiveReconcile {
26 interval: reconcile.interval.clone(),
27 auto_apply: reconcile.auto_apply,
28 drift_policy: reconcile.drift_policy.clone(),
29 };
30
31 if let Some(patch) = reconcile
33 .patches
34 .iter()
35 .rev()
36 .find(|p| p.kind == config::ReconcilePatchKind::Profile && p.name.is_none())
37 {
38 overlay_reconcile_patch(&mut effective, patch);
39 }
40
41 for profile_name in profile_chain {
43 if let Some(patch) = reconcile.patches.iter().rev().find(|p| {
44 p.kind == config::ReconcilePatchKind::Profile && p.name.as_deref() == Some(profile_name)
45 }) {
46 overlay_reconcile_patch(&mut effective, patch);
47 }
48 }
49
50 if let Some(patch) = reconcile
52 .patches
53 .iter()
54 .rev()
55 .find(|p| p.kind == config::ReconcilePatchKind::Module && p.name.is_none())
56 {
57 overlay_reconcile_patch(&mut effective, patch);
58 }
59
60 if let Some(patch) = reconcile.patches.iter().rev().find(|p| {
62 p.kind == config::ReconcilePatchKind::Module && p.name.as_deref() == Some(module_name)
63 }) {
64 overlay_reconcile_patch(&mut effective, patch);
65 }
66
67 effective
68}
69
70fn overlay_reconcile_patch(base: &mut EffectiveReconcile, patch: &config::ReconcilePatch) {
72 if let Some(ref interval) = patch.interval {
73 base.interval = interval.clone();
74 }
75 if let Some(auto_apply) = patch.auto_apply {
76 base.auto_apply = auto_apply;
77 }
78 if let Some(ref dp) = patch.drift_policy {
79 base.drift_policy = dp.clone();
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 fn make_reconcile_config(patches: Vec<config::ReconcilePatch>) -> config::ReconcileConfig {
88 config::ReconcileConfig {
89 interval: "5m".into(),
90 on_change: false,
91 auto_apply: false,
92 policy: None,
93 drift_policy: config::DriftPolicy::NotifyOnly,
94 patches,
95 }
96 }
97
98 #[test]
99 fn no_patches_returns_global_defaults() {
100 let rc = make_reconcile_config(vec![]);
101 let eff = resolve_effective_reconcile("docker", &["base"], &rc);
102 assert_eq!(eff.interval, "5m");
103 assert!(!eff.auto_apply);
104 assert_eq!(eff.drift_policy, config::DriftPolicy::NotifyOnly);
105 }
106
107 #[test]
108 fn named_module_patch_overrides_global() {
109 let rc = make_reconcile_config(vec![config::ReconcilePatch {
110 kind: config::ReconcilePatchKind::Module,
111 name: Some("docker".into()),
112 interval: Some("1m".into()),
113 auto_apply: Some(true),
114 drift_policy: Some(config::DriftPolicy::Auto),
115 }]);
116 let eff = resolve_effective_reconcile("docker", &["base"], &rc);
117 assert_eq!(eff.interval, "1m");
118 assert!(eff.auto_apply);
119 assert_eq!(eff.drift_policy, config::DriftPolicy::Auto);
120 }
121
122 #[test]
123 fn named_module_patch_does_not_affect_other_modules() {
124 let rc = make_reconcile_config(vec![config::ReconcilePatch {
125 kind: config::ReconcilePatchKind::Module,
126 name: Some("docker".into()),
127 interval: Some("1m".into()),
128 auto_apply: None,
129 drift_policy: None,
130 }]);
131 let eff = resolve_effective_reconcile("kubernetes", &["base"], &rc);
132 assert_eq!(eff.interval, "5m");
133 }
134
135 #[test]
136 fn kind_wide_module_patch_applies_to_all() {
137 let rc = make_reconcile_config(vec![config::ReconcilePatch {
138 kind: config::ReconcilePatchKind::Module,
139 name: None,
140 interval: Some("2m".into()),
141 auto_apply: None,
142 drift_policy: None,
143 }]);
144 let eff = resolve_effective_reconcile("anything", &["base"], &rc);
145 assert_eq!(eff.interval, "2m");
146 }
147
148 #[test]
149 fn named_profile_patch_applies_when_in_chain() {
150 let rc = make_reconcile_config(vec![config::ReconcilePatch {
151 kind: config::ReconcilePatchKind::Profile,
152 name: Some("work".into()),
153 interval: Some("10m".into()),
154 auto_apply: Some(true),
155 drift_policy: None,
156 }]);
157 let eff = resolve_effective_reconcile("docker", &["base", "work"], &rc);
158 assert_eq!(eff.interval, "10m");
159 assert!(eff.auto_apply);
160 }
161
162 #[test]
163 fn named_module_beats_named_profile() {
164 let rc = make_reconcile_config(vec![
165 config::ReconcilePatch {
166 kind: config::ReconcilePatchKind::Profile,
167 name: Some("work".into()),
168 interval: Some("10m".into()),
169 auto_apply: None,
170 drift_policy: None,
171 },
172 config::ReconcilePatch {
173 kind: config::ReconcilePatchKind::Module,
174 name: Some("docker".into()),
175 interval: Some("30s".into()),
176 auto_apply: None,
177 drift_policy: None,
178 },
179 ]);
180 let eff = resolve_effective_reconcile("docker", &["base", "work"], &rc);
181 assert_eq!(eff.interval, "30s");
182 }
183
184 #[test]
185 fn kind_wide_profile_patch_applies() {
186 let rc = make_reconcile_config(vec![config::ReconcilePatch {
187 kind: config::ReconcilePatchKind::Profile,
188 name: None,
189 interval: None,
190 auto_apply: Some(true),
191 drift_policy: Some(config::DriftPolicy::Auto),
192 }]);
193 let eff = resolve_effective_reconcile("docker", &["base"], &rc);
194 assert!(eff.auto_apply);
195 assert_eq!(eff.drift_policy, config::DriftPolicy::Auto);
196 }
197}