fakecloud_cloudformation/template/
resolution.rs1use super::*;
4
5pub fn resolve_resource_properties(
7 resource: &ResourceDefinition,
8 template_body: &str,
9 parameters: &BTreeMap<String, String>,
10 resource_physical_ids: &BTreeMap<String, String>,
11) -> Result<ResourceDefinition, String> {
12 resolve_resource_properties_with_attrs(
13 resource,
14 template_body,
15 parameters,
16 resource_physical_ids,
17 &BTreeMap::new(),
18 &BTreeMap::new(),
19 )
20}
21
22pub fn resolve_resource_properties_with_attrs(
25 resource: &ResourceDefinition,
26 template_body: &str,
27 parameters: &BTreeMap<String, String>,
28 resource_physical_ids: &BTreeMap<String, String>,
29 resource_attributes: &BTreeMap<String, BTreeMap<String, String>>,
30 imports: &BTreeMap<String, String>,
31) -> Result<ResourceDefinition, String> {
32 let value: Value = if template_body.trim_start().starts_with('{') {
33 serde_json::from_str(template_body).map_err(|e| format!("Invalid JSON template: {e}"))?
34 } else {
35 serde_yaml::from_str(template_body).map_err(|e| format!("Invalid YAML template: {e}"))?
36 };
37 let value = expand_for_each(&value, &BTreeMap::new(), parameters)?;
40 let value = expand_sam(&value);
46
47 let resources_obj = value
48 .get("Resources")
49 .and_then(|v| v.as_object())
50 .ok_or("Template must contain a Resources section")?;
51
52 let raw_props = resources_obj
53 .get(&resource.logical_id)
54 .and_then(|r| r.get("Properties"))
55 .cloned()
56 .unwrap_or(Value::Object(serde_json::Map::new()));
57
58 let conditions = evaluate_conditions(&value, parameters)?;
63 let mappings = parse_mappings(&value);
64 let raw_props = apply_mappings(&raw_props, parameters, &mappings, &conditions)?;
65
66 let resolved = resolve_refs_full(
67 &raw_props,
68 parameters,
69 resources_obj,
70 resource_physical_ids,
71 resource_attributes,
72 imports,
73 &conditions,
74 );
75 let resolved = strip_no_value(resolved);
76
77 Ok(ResourceDefinition {
78 logical_id: resource.logical_id.clone(),
79 resource_type: resource.resource_type.clone(),
80 properties: resolved,
81 })
82}
83
84pub fn dependency_order(
103 template_body: &str,
104 parameters: &BTreeMap<String, String>,
105 resource_defs: &[ResourceDefinition],
106) -> Vec<usize> {
107 let n = resource_defs.len();
108 let identity = || (0..n).collect::<Vec<usize>>();
109 if n < 2 {
110 return identity();
111 }
112
113 let parse = |body: &str| -> Result<Value, ()> {
114 if body.trim_start().starts_with('{') {
115 serde_json::from_str(body).map_err(|_| ())
116 } else {
117 serde_yaml::from_str(body).map_err(|_| ())
118 }
119 };
120 let Ok(value) = parse(template_body) else {
121 return identity();
122 };
123 let Ok(value) = expand_for_each(&value, &BTreeMap::new(), parameters) else {
125 return identity();
126 };
127 let value = expand_sam(&value);
128 let Some(resources_obj) = value.get("Resources").and_then(|v| v.as_object()) else {
129 return identity();
130 };
131
132 let index_of: BTreeMap<&str, usize> = resource_defs
133 .iter()
134 .enumerate()
135 .map(|(i, r)| (r.logical_id.as_str(), i))
136 .collect();
137 let known: BTreeSet<&str> = index_of.keys().copied().collect();
138
139 let mut deps: Vec<BTreeSet<usize>> = vec![BTreeSet::new(); n];
141 for (i, r) in resource_defs.iter().enumerate() {
142 let Some(raw) = resources_obj.get(&r.logical_id) else {
143 continue;
144 };
145 let mut refs: BTreeSet<String> = BTreeSet::new();
146 match raw.get("DependsOn") {
147 Some(Value::String(s)) => {
148 refs.insert(s.clone());
149 }
150 Some(Value::Array(a)) => {
151 for x in a {
152 if let Some(s) = x.as_str() {
153 refs.insert(s.to_string());
154 }
155 }
156 }
157 _ => {}
158 }
159 if let Some(props) = raw.get("Properties") {
160 collect_resource_refs(props, &known, &mut refs);
161 }
162 for name in refs {
163 if let Some(&j) = index_of.get(name.as_str()) {
164 if j != i {
165 deps[i].insert(j);
166 }
167 }
168 }
169 }
170
171 let mut indeg: Vec<usize> = deps.iter().map(|d| d.len()).collect();
173 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); n];
174 for (i, d) in deps.iter().enumerate() {
175 for &j in d {
176 dependents[j].push(i);
177 }
178 }
179 let mut ready: BTreeSet<usize> = (0..n).filter(|&i| indeg[i] == 0).collect();
180 let mut order: Vec<usize> = Vec::with_capacity(n);
181 while let Some(&i) = ready.iter().next() {
182 ready.remove(&i);
183 order.push(i);
184 for &j in &dependents[i] {
185 indeg[j] -= 1;
186 if indeg[j] == 0 {
187 ready.insert(j);
188 }
189 }
190 }
191 if order.len() != n {
192 let placed: BTreeSet<usize> = order.iter().copied().collect();
194 for i in 0..n {
195 if !placed.contains(&i) {
196 order.push(i);
197 }
198 }
199 }
200 order
201}
202
203fn collect_resource_refs(value: &Value, known: &BTreeSet<&str>, out: &mut BTreeSet<String>) {
206 match value {
207 Value::Object(map) => {
208 if let Some(Value::String(name)) = map.get("Ref") {
209 if known.contains(name.as_str()) {
210 out.insert(name.clone());
211 }
212 }
213 if let Some(g) = map.get("Fn::GetAtt") {
214 let logical = match g {
215 Value::Array(a) => a.first().and_then(|x| x.as_str()),
216 Value::String(s) => s.split('.').next(),
217 _ => None,
218 };
219 if let Some(l) = logical {
220 if known.contains(l) {
221 out.insert(l.to_string());
222 }
223 }
224 }
225 if let Some(s) = map.get("Fn::Sub") {
226 collect_sub_refs(s, known, out);
227 }
228 for v in map.values() {
230 collect_resource_refs(v, known, out);
231 }
232 }
233 Value::Array(a) => {
234 for v in a {
235 collect_resource_refs(v, known, out);
236 }
237 }
238 _ => {}
239 }
240}
241
242fn collect_sub_refs(sub: &Value, known: &BTreeSet<&str>, out: &mut BTreeSet<String>) {
246 let (template, local_vars): (Option<&str>, BTreeSet<&str>) = match sub {
247 Value::String(t) => (Some(t.as_str()), BTreeSet::new()),
248 Value::Array(a) => {
249 let t = a.first().and_then(|x| x.as_str());
250 let vars = a
251 .get(1)
252 .and_then(|x| x.as_object())
253 .map(|m| m.keys().map(|k| k.as_str()).collect())
254 .unwrap_or_default();
255 (t, vars)
256 }
257 _ => (None, BTreeSet::new()),
258 };
259 let Some(t) = template else {
260 return;
261 };
262 let mut i = 0;
263 while let Some(start) = t[i..].find("${") {
264 let abs = i + start + 2;
265 let Some(end_rel) = t[abs..].find('}') else {
266 break;
267 };
268 let token = &t[abs..abs + end_rel];
269 if !token.starts_with('!') {
271 let logical = token.split('.').next().unwrap_or(token).trim();
272 if !local_vars.contains(logical) && known.contains(logical) {
273 out.insert(logical.to_string());
274 }
275 }
276 i = abs + end_rel + 1;
277 }
278}
279
280#[cfg(test)]
281mod dependency_order_tests {
282 use super::*;
283
284 fn rd(logical: &str, resource_type: &str) -> ResourceDefinition {
285 ResourceDefinition {
286 logical_id: logical.to_string(),
287 resource_type: resource_type.to_string(),
288 properties: serde_json::json!({}),
289 }
290 }
291
292 #[test]
296 fn referenced_resource_is_ordered_first() {
297 let template = r#"{
298 "Resources": {
299 "Machine": {
300 "Type": "AWS::StepFunctions::StateMachine",
301 "Properties": {
302 "DefinitionString": "${fn}",
303 "DefinitionSubstitutions": {"fn": {"Ref": "WorkerFn"}}
304 }
305 },
306 "WorkerFn": {"Type": "AWS::Lambda::Function", "Properties": {}}
307 }
308 }"#;
309 let defs = vec![
311 rd("Machine", "AWS::StepFunctions::StateMachine"),
312 rd("WorkerFn", "AWS::Lambda::Function"),
313 ];
314 let order = dependency_order(template, &BTreeMap::new(), &defs);
315 assert_eq!(
316 order,
317 vec![1, 0],
318 "WorkerFn must be provisioned before Machine"
319 );
320 }
321
322 #[test]
323 fn get_att_sub_and_depends_on_all_create_edges() {
324 let template = r#"{
326 "Resources": {
327 "B": {"Type": "X", "Properties": {"V": {"Fn::GetAtt": ["A", "Arn"]}}},
328 "C": {"Type": "X", "Properties": {"V": {"Fn::Sub": "p-${A}-s"}}},
329 "D": {"Type": "X", "DependsOn": "A", "Properties": {}},
330 "A": {"Type": "X", "Properties": {}}
331 }
332 }"#;
333 let defs = vec![rd("B", "X"), rd("C", "X"), rd("D", "X"), rd("A", "X")];
334 let order = dependency_order(template, &BTreeMap::new(), &defs);
335 let pos = |name: &str| {
336 order
337 .iter()
338 .position(|&i| defs[i].logical_id == name)
339 .unwrap()
340 };
341 assert!(pos("A") < pos("B"), "GetAtt edge");
342 assert!(pos("A") < pos("C"), "Fn::Sub edge");
343 assert!(pos("A") < pos("D"), "DependsOn edge");
344 }
345
346 #[test]
347 fn import_value_in_property_resolves_from_imports() {
348 let template = r#"{
351 "Resources": {
352 "Q": {"Type": "AWS::SQS::Queue", "Properties": {"QueueName": {"Fn::ImportValue": "SharedQueueName"}}}
353 }
354 }"#;
355 let mut imports = BTreeMap::new();
356 imports.insert("SharedQueueName".to_string(), "shared-q".to_string());
357 let resolved = resolve_resource_properties_with_attrs(
358 &rd("Q", "AWS::SQS::Queue"),
359 template,
360 &BTreeMap::new(),
361 &BTreeMap::new(),
362 &BTreeMap::new(),
363 &imports,
364 )
365 .unwrap();
366 assert_eq!(
367 resolved.properties["QueueName"],
368 serde_json::json!("shared-q")
369 );
370 }
371
372 #[test]
373 fn independent_resources_keep_original_order() {
374 let template =
375 r#"{"Resources": {"A": {"Type": "X"}, "B": {"Type": "X"}, "C": {"Type": "X"}}}"#;
376 let defs = vec![rd("A", "X"), rd("B", "X"), rd("C", "X")];
377 assert_eq!(
378 dependency_order(template, &BTreeMap::new(), &defs),
379 vec![0, 1, 2]
380 );
381 }
382
383 #[test]
384 fn sub_escape_and_local_vars_are_not_edges() {
385 let template = r#"{
388 "Resources": {
389 "A": {"Type": "X", "Properties": {}},
390 "B": {"Type": "X", "Properties": {
391 "V": {"Fn::Sub": ["${!A}-${V}", {"V": "literal"}]}
392 }}
393 }
394 }"#;
395 let defs = vec![rd("A", "X"), rd("B", "X")];
396 assert_eq!(
398 dependency_order(template, &BTreeMap::new(), &defs),
399 vec![0, 1]
400 );
401 }
402
403 #[test]
404 fn dependency_cycle_falls_back_to_original_order() {
405 let template = r#"{
406 "Resources": {
407 "A": {"Type": "X", "Properties": {"V": {"Ref": "B"}}},
408 "B": {"Type": "X", "Properties": {"V": {"Ref": "A"}}}
409 }
410 }"#;
411 let defs = vec![rd("A", "X"), rd("B", "X")];
412 assert_eq!(
413 dependency_order(template, &BTreeMap::new(), &defs),
414 vec![0, 1]
415 );
416 }
417}