From 53ace58e44e9840ee0d8a357803b078f3e9cf587 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 11 Feb 2026 09:30:35 +0100 Subject: [PATCH] bootstrap: add `--single-node` flag that creates a single-node layout --- src/garage/cli/structs.rs | 13 +++++++- src/garage/main.rs | 4 +-- src/garage/server.rs | 70 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 7fd2defa..4bb6dc0a 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -8,7 +8,7 @@ use crate::cli::local::convert_db; pub enum Command { /// Run Garage server #[structopt(name = "server", version = garage_version())] - Server, + Server(ServerOpt), /// Get network status #[structopt(name = "status", version = garage_version())] @@ -79,6 +79,17 @@ pub enum Command { }, } +// --------------------------- +// ---- garage server ... ---- +// --------------------------- + +#[derive(StructOpt, Debug)] +pub struct ServerOpt { + /// Automatically configure a single-node layout in the cluster + #[structopt(long = "single-node")] + pub(crate) single_node: bool, +} + // ------------------------- // ---- garage node ... ---- // ------------------------- diff --git a/src/garage/main.rs b/src/garage/main.rs index 6a8c9412..0ab64086 100644 --- a/src/garage/main.rs +++ b/src/garage/main.rs @@ -158,7 +158,7 @@ fn main() { async fn run(opt: Opt) -> Result<(), Error> { match opt.cmd { - Command::Server => server::run_server(opt.config_file, opt.secrets).await, + Command::Server(sopt) => server::run_server(opt.config_file, opt.secrets, sopt).await, Command::OfflineRepair(repair_opt) => { cli::local::repair::offline_repair(opt.config_file, opt.secrets, repair_opt).await } @@ -188,7 +188,7 @@ async fn run(opt: Opt) -> Result<(), Error> { fn init_logging(opt: &Opt) { if std::env::var("RUST_LOG").is_err() { let default_log = match &opt.cmd { - Command::Server => "netapp=info,garage=info", + Command::Server(_) => "netapp=info,garage=info", _ => "netapp=warn,garage=warn", }; diff --git a/src/garage/server.rs b/src/garage/server.rs index f9cd5631..7f80b205 100644 --- a/src/garage/server.rs +++ b/src/garage/server.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use std::sync::Arc; use tokio::sync::watch; @@ -16,6 +17,7 @@ use garage_api_k2v::api_server::K2VApiServer; use crate::secrets::{fill_secrets, Secrets}; use crate::tracing_setup::init_tracing; +use crate::ServerOpt; async fn wait_from(mut chan: watch::Receiver) { while !*chan.borrow() { @@ -25,7 +27,11 @@ async fn wait_from(mut chan: watch::Receiver) { } } -pub async fn run_server(config_file: PathBuf, secrets: Secrets) -> Result<(), Error> { +pub async fn run_server( + config_file: PathBuf, + secrets: Secrets, + opt: ServerOpt, +) -> Result<(), Error> { info!("Loading configuration..."); let config = fill_secrets(read_config(config_file)?, secrets)?; @@ -67,6 +73,10 @@ pub async fn run_server(config_file: PathBuf, secrets: Secrets) -> Result<(), Er info!("Launching internal Garage cluster communications..."); let run_system = tokio::spawn(garage.system.clone().run(watch_cancel.clone())); + // ---- Run initial configuration logic ---- + + initial_config(&garage, opt).await?; + // ---- Launch public-facing API servers ---- let mut servers = vec![]; @@ -164,6 +174,64 @@ pub async fn run_server(config_file: PathBuf, secrets: Secrets) -> Result<(), Er Ok(()) } +async fn initial_config(garage: &Arc, opt: ServerOpt) -> Result<(), Error> { + use garage_rpc::layout::*; + + if opt.single_node { + let layout_version = garage.system.cluster_layout().inner().current().version; + + if layout_version > 1 { + return Err(Error::Message("Refusing to run in single-node mode: layout version is already superior to 1. Remove the --single-node flag to run the server in full mode.".into())); + } + + if layout_version == 0 { + // Setup initial layout + let mut layout = garage.system.cluster_layout().inner().clone(); + let our_id = garage.system.id; + + // Check no other nodes are present in the system + let nodes = garage.system.get_known_nodes(); + if nodes.iter().any(|x| x.id != our_id) { + return Err(Error::Message("Refusing to run in single-node mode: more nodes are already present in the cluster.".into())); + } + + // Automatically determine this node's capacity + let capacity = garage + .system + .local_status() + .data_disk_avail + .map(|(_avail, total)| total) + .unwrap_or(1024 * 1024 * 1024); // Default to 1GB + + assert!(layout.current().roles.is_empty()); + + layout.staging.get_mut().roles.clear(); + layout.staging.get_mut().roles.update_in_place( + our_id, + NodeRoleV(Some(NodeRole { + zone: "dc1".to_string(), + capacity: Some(capacity), + tags: vec!["default".to_string()], + })), + ); + + let (layout, msg) = layout.apply_staged_changes(1)?; + info!( + "Created initial layout for single-node configuration:\n{}", + msg.join("\n") + ); + + garage + .system + .layout_manager + .update_cluster_layout(&layout) + .await?; + } + } + + Ok(()) +} + #[cfg(unix)] fn watch_shutdown_signal() -> watch::Receiver { use tokio::signal::unix::*;