|
|
@@ -0,0 +1,74 @@
|
|
|
+//! Deployment discovery — query running containers across all nodes and
|
|
|
+//! group them into deployment variants for disambiguation.
|
|
|
+
|
|
|
+use std::collections::{HashMap, HashSet};
|
|
|
+
|
|
|
+use crate::config::Config;
|
|
|
+use crate::ssh::run_ssh;
|
|
|
+
|
|
|
+/// A running deployment variant — a set of nodes on a cluster running a recipe.
|
|
|
+pub struct DeploymentVariant {
|
|
|
+ pub cluster: String,
|
|
|
+ pub nodes: Vec<String>,
|
|
|
+}
|
|
|
+
|
|
|
+/// Discover running containers for a recipe across all unique nodes.
|
|
|
+/// Groups into deployment variants by (cluster, node-set).
|
|
|
+pub async fn discover_recipe_deployments(
|
|
|
+ config: &Config,
|
|
|
+ recipe_name: &str,
|
|
|
+) -> anyhow::Result<Vec<DeploymentVariant>> {
|
|
|
+ let mut seen = HashSet::new();
|
|
|
+ let mut handles = Vec::new();
|
|
|
+ for cluster in &config.clusters {
|
|
|
+ for node in &cluster.nodes {
|
|
|
+ if seen.contains(&node.ssh_ip) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ seen.insert(node.ssh_ip.clone());
|
|
|
+ let cmd = format!(
|
|
|
+ "docker ps --filter label=sm121.recipe={} \
|
|
|
+ --format '{{{{.Label \"sm121.cluster\"}}}}|{{{{.Names}}}}' 2>/dev/null",
|
|
|
+ recipe_name
|
|
|
+ );
|
|
|
+ let config = config.clone();
|
|
|
+ let node = node.clone();
|
|
|
+ let name = node.name.clone();
|
|
|
+ handles.push(tokio::spawn(async move {
|
|
|
+ let out = run_ssh(&config, &node, cmd, false)
|
|
|
+ .await
|
|
|
+ .unwrap_or_default();
|
|
|
+ (name, out)
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let mut by_cluster: HashMap<String, HashSet<String>> = HashMap::new();
|
|
|
+ for h in futures::future::join_all(handles).await {
|
|
|
+ let (node_name, output) = h.unwrap_or_default();
|
|
|
+ for line in output.lines() {
|
|
|
+ let line = line.trim();
|
|
|
+ if line.is_empty() {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ let mut parts = line.splitn(2, '|');
|
|
|
+ let cluster = parts.next().unwrap_or("").to_string();
|
|
|
+ if cluster.is_empty() {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ by_cluster
|
|
|
+ .entry(cluster)
|
|
|
+ .or_default()
|
|
|
+ .insert(node_name.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let mut variants: Vec<DeploymentVariant> = by_cluster
|
|
|
+ .into_iter()
|
|
|
+ .map(|(cluster, nodes)| {
|
|
|
+ let mut nodes: Vec<String> = nodes.into_iter().collect();
|
|
|
+ nodes.sort();
|
|
|
+ DeploymentVariant { cluster, nodes }
|
|
|
+ })
|
|
|
+ .collect();
|
|
|
+ variants.sort_by(|a, b| a.cluster.cmp(&b.cluster));
|
|
|
+ Ok(variants)
|
|
|
+}
|