1use std::collections::{HashMap, HashSet, VecDeque};
2
3use crate::error::{DeployError, Result};
4use crate::types::ServiceConfigView;
5
6#[derive(Debug, Clone)]
7pub struct ServiceNode {
8 pub name: String,
9 pub depends_on: Vec<String>,
10}
11
12#[derive(Debug, Clone, Default)]
13pub struct ServiceGraph {
14 pub nodes: HashMap<String, ServiceNode>,
15}
16
17impl ServiceGraph {
18 pub fn from_services(services: &[ServiceConfigView]) -> Self {
19 let mut nodes = HashMap::new();
20 for svc in services {
21 nodes.insert(
22 svc.name.clone(),
23 ServiceNode {
24 name: svc.name.clone(),
25 depends_on: svc.depends_on.clone(),
26 },
27 );
28 }
29 Self { nodes }
30 }
31}
32
33pub fn order_services(
39 selected: &[String],
40 group_order: Option<&[String]>,
41 graph: &ServiceGraph,
42) -> Result<Vec<String>> {
43 if let Some(order) = group_order {
44 if !order.is_empty() {
45 let selected_set: HashSet<&str> = selected.iter().map(String::as_str).collect();
46 let mut out = Vec::new();
47 for name in order {
48 if selected_set.contains(name.as_str()) {
49 out.push(name.clone());
50 }
51 }
52 for name in selected {
54 if !out.iter().any(|n| n == name) {
55 out.push(name.clone());
56 }
57 }
58 return Ok(out);
59 }
60 }
61
62 let selected_set: HashSet<String> = selected.iter().cloned().collect();
64 for name in selected {
65 let Some(node) = graph.nodes.get(name) else {
66 continue;
67 };
68 for dep in &node.depends_on {
69 if !graph.nodes.contains_key(dep) {
70 return Err(DeployError::Graph(format!(
71 "service `{name}` depends on missing service `{dep}`"
72 )));
73 }
74 }
75 }
76
77 let mut indegree: HashMap<String, usize> = selected
79 .iter()
80 .map(|n| (n.clone(), 0usize))
81 .collect();
82 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
83
84 for name in selected {
85 let deps = graph
86 .nodes
87 .get(name)
88 .map(|n| n.depends_on.clone())
89 .unwrap_or_default();
90 for dep in deps {
91 if !selected_set.contains(&dep) {
92 continue;
93 }
94 adj.entry(dep.clone()).or_default().push(name.clone());
95 *indegree.entry(name.clone()).or_default() += 1;
96 }
97 }
98
99 let mut queue: VecDeque<String> = selected
100 .iter()
101 .filter(|n| indegree.get(*n).copied().unwrap_or(0) == 0)
102 .cloned()
103 .collect();
104
105 queue.make_contiguous().sort_by_key(|n| {
107 selected.iter().position(|s| s == n).unwrap_or(usize::MAX)
108 });
109
110 let mut ordered = Vec::new();
111 while let Some(node) = queue.pop_front() {
112 ordered.push(node.clone());
113 if let Some(children) = adj.get(&node) {
114 for child in children {
115 if let Some(d) = indegree.get_mut(child) {
116 *d = d.saturating_sub(1);
117 if *d == 0 {
118 queue.push_back(child.clone());
119 }
120 }
121 }
122 }
123 }
124
125 if ordered.len() != selected.len() {
126 return Err(DeployError::Graph(
127 "dependency cycle detected among selected services".into(),
128 ));
129 }
130 Ok(ordered)
131}
132
133pub fn enforce_athena_operator_before_runtime(order: &mut Vec<String>) {
135 let op = order.iter().position(|s| s == "athena-operator");
136 let rt = order.iter().position(|s| s == "athena-runtime");
137 if let (Some(i), Some(j)) = (op, rt) {
138 if i > j {
139 order.swap(i, j);
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn svc(name: &str, deps: &[&str]) -> ServiceConfigView {
149 ServiceConfigView {
150 environment: Default::default(),
151 port: None,
152 name: name.into(),
153 root_directory: None,
154 version: None,
155 depends_on: deps.iter().map(|s| (*s).into()).collect(),
156 oci: None,
157 deploy: None,
158 }
159 }
160
161 #[test]
162 fn group_order_authoritative() {
163 let services = vec![svc("a", &[]), svc("b", &[]), svc("c", &[])];
164 let graph = ServiceGraph::from_services(&services);
165 let order = order_services(
166 &["a".into(), "b".into(), "c".into()],
167 Some(&["c".into(), "a".into(), "b".into()]),
168 &graph,
169 )
170 .unwrap();
171 assert_eq!(order, vec!["c", "a", "b"]);
172 }
173
174 #[test]
175 fn topo_respects_depends_on() {
176 let services = vec![svc("a", &[]), svc("b", &["a"]), svc("c", &["b"])];
177 let graph = ServiceGraph::from_services(&services);
178 let order = order_services(
179 &["c".into(), "b".into(), "a".into()],
180 None,
181 &graph,
182 )
183 .unwrap();
184 assert_eq!(order, vec!["a", "b", "c"]);
185 }
186
187 #[test]
188 fn cycle_errors() {
189 let services = vec![svc("a", &["b"]), svc("b", &["a"])];
190 let graph = ServiceGraph::from_services(&services);
191 let err = order_services(&["a".into(), "b".into()], None, &graph).unwrap_err();
192 assert!(err.to_string().contains("cycle"));
193 }
194
195 #[test]
196 fn athena_guardrail() {
197 let mut order = vec!["athena-runtime".into(), "athena-operator".into()];
198 enforce_athena_operator_before_runtime(&mut order);
199 assert_eq!(order[0], "athena-operator");
200 }
201}