|
@@ -4,11 +4,12 @@ use tracing::{info, warn};
|
|
|
use crate::config::{Cluster, Config, OnFailure, PreServeCommand, Recipe};
|
|
use crate::config::{Cluster, Config, OnFailure, PreServeCommand, Recipe};
|
|
|
|
|
|
|
|
/// Run pre-serve commands on every node of the cluster before launching
|
|
/// Run pre-serve commands on every node of the cluster before launching
|
|
|
-/// containers or tmux sessions. Combines cluster-level config
|
|
|
|
|
-/// (`[before_serve."<cluster>"]`) with recipe-level overrides
|
|
|
|
|
-/// (`before_serve = [...]` in recipe.toml). Cluster-level commands run
|
|
|
|
|
-/// first, then recipe-level — each in declared order — so the resulting
|
|
|
|
|
-/// state is predictable.
|
|
|
|
|
|
|
+/// containers or tmux sessions. Combines config-level hooks with
|
|
|
|
|
+/// recipe-level overrides (`before_serve = [...]` in recipe.toml). Config
|
|
|
|
|
+/// hooks come from `[before_serve."*"]` (global — runs for every cluster)
|
|
|
|
|
+/// followed by `[before_serve."<cluster>"]` (per-cluster), then recipe —
|
|
|
|
|
+/// each in declared order — so per-cluster can supplement/override the
|
|
|
|
|
+/// global list and recipe commands run last.
|
|
|
///
|
|
///
|
|
|
/// Per-command behaviour on a non-zero exit is controlled by
|
|
/// Per-command behaviour on a non-zero exit is controlled by
|
|
|
/// [`PreServeCommand::on_failure`]:
|
|
/// [`PreServeCommand::on_failure`]:
|
|
@@ -81,6 +82,12 @@ fn combined_commands(
|
|
|
recipe: &Recipe,
|
|
recipe: &Recipe,
|
|
|
) -> Vec<(&'static str, PreServeCommand)> {
|
|
) -> Vec<(&'static str, PreServeCommand)> {
|
|
|
let mut out: Vec<(&'static str, PreServeCommand)> = Vec::new();
|
|
let mut out: Vec<(&'static str, PreServeCommand)> = Vec::new();
|
|
|
|
|
+ // Global hooks (literal "*" key — not a glob): run for every cluster.
|
|
|
|
|
+ if let Some(table) = config.before_serve.get("*") {
|
|
|
|
|
+ for c in &table.commands {
|
|
|
|
|
+ out.push(("global", c.clone()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if let Some(table) = config.before_serve.get(&cluster.name) {
|
|
if let Some(table) = config.before_serve.get(&cluster.name) {
|
|
|
for c in &table.commands {
|
|
for c in &table.commands {
|
|
|
out.push(("cluster", c.clone()));
|
|
out.push(("cluster", c.clone()));
|
|
@@ -255,6 +262,57 @@ mod tests {
|
|
|
assert_eq!(truncate(s, 2), "你好…");
|
|
assert_eq!(truncate(s, 2), "你好…");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn combined_runs_global_then_cluster_then_recipe() {
|
|
|
|
|
+ let mut bs = HashMap::new();
|
|
|
|
|
+ bs.insert(
|
|
|
|
|
+ "c1".to_string(),
|
|
|
|
|
+ crate::config::BeforeServeTable {
|
|
|
|
|
+ commands: vec![PreServeCommand::Bare("cluster_cmd".into())],
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
+ bs.insert(
|
|
|
|
|
+ "*".to_string(),
|
|
|
|
|
+ crate::config::BeforeServeTable {
|
|
|
|
|
+ commands: vec![PreServeCommand::Bare("global_cmd".into())],
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
+ let cfg = test_config(bs);
|
|
|
|
|
+ let recipe = Recipe {
|
|
|
|
|
+ before_serve: vec![PreServeCommand::Bare("recipe_cmd".into())],
|
|
|
|
|
+ ..test_recipe()
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let cmds = combined_commands(&cfg, &test_cluster(), &recipe);
|
|
|
|
|
+ let names: Vec<&str> = cmds.iter().map(|(_, c)| c.command()).collect();
|
|
|
|
|
+ let origins: Vec<&str> = cmds.iter().map(|(o, _)| *o).collect();
|
|
|
|
|
+ assert_eq!(names, vec!["global_cmd", "cluster_cmd", "recipe_cmd"]);
|
|
|
|
|
+ assert_eq!(origins, vec!["global", "cluster", "recipe"]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn combined_global_applies_to_unlisted_cluster() {
|
|
|
|
|
+ let mut bs = HashMap::new();
|
|
|
|
|
+ bs.insert(
|
|
|
|
|
+ "*".to_string(),
|
|
|
|
|
+ crate::config::BeforeServeTable {
|
|
|
|
|
+ commands: vec![PreServeCommand::Bare("global_cmd".into())],
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
+ // Cluster "c1" has no specific entry — only the global hook runs.
|
|
|
|
|
+ let cfg = test_config(bs);
|
|
|
|
|
+ let recipe = Recipe {
|
|
|
|
|
+ before_serve: vec![],
|
|
|
|
|
+ after_stop: vec![],
|
|
|
|
|
+ solo: None,
|
|
|
|
|
+ ..test_recipe()
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let cmds = combined_commands(&cfg, &test_cluster(), &recipe);
|
|
|
|
|
+ let names: Vec<&str> = cmds.iter().map(|(_, c)| c.command()).collect();
|
|
|
|
|
+ assert_eq!(names, vec!["global_cmd"]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
#[test]
|
|
#[test]
|
|
|
fn combined_orders_cluster_then_recipe() {
|
|
fn combined_orders_cluster_then_recipe() {
|
|
|
let mut bs = HashMap::new();
|
|
let mut bs = HashMap::new();
|