git.delta.rocks / jrsonnet / refs/commits / a8825de2d893

difftreelog

feat manual connection destination

ruzwnouyYaroslav Bolyukin2025-07-18parent: #c6d5484.patch.diff
in: trunk

2 files changed

modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
after · cmds/fleet/src/cmds/build_systems.rs
1use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};23use anyhow::{Result, anyhow};4use clap::Parser;5use fleet_base::{6	deploy::{DeployAction, deploy_task, upload_task},7	host::{Config, DeployKind, GenerationStorage},8	opts::FleetOpts,9};10use nix_eval::{NixBuildBatch, nix_go};11use tokio::task::LocalSet;12use tracing::{Instrument, error, field, info, info_span, warn};1314#[derive(Parser)]15pub struct Deploy {16	/// Disable automatic rollback17	#[clap(long)]18	disable_rollback: bool,19	/// Action to execute after system is built20	action: DeployAction,21}2223#[derive(Parser, Clone)]24pub struct BuildSystems {25	/// Attribute to build. Systems are deployed from "toplevel" attr, well-known used attributes26	/// are "sdImage"/"isoImage", and your configuration may include any other build attributes.27	#[clap(long, default_value = "toplevel")]28	build_attr: String,29}3031async fn build_task(32	config: Config,33	hostname: String,34	build_attr: &str,35	batch: Option<NixBuildBatch>,36) -> Result<PathBuf> {37	info!("building");38	let host = config.host(&hostname).await?;39	// let action = Action::from(self.subcommand.clone());40	let nixos = host.nixos_config().await?;41	let drv = nix_go!(nixos.system.build[{ build_attr }]);42	let outputs = drv.build_maybe_batch(batch).await?;43	let out_output = outputs44		.get("out")45		.ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;4647	// We already have system profiles for backups.48	if !host.local {49		info!("adding gc root");50		let mut cmd = config.local_host().cmd("nix").await?;51		cmd.arg("build")52			.comparg(53				"--profile",54				format!(55					"/nix/var/nix/profiles/{}-{hostname}",56					config.data().gc_root_prefix57				),58			)59			.arg(out_output);60		cmd.sudo().run_nix().await?;61	}6263	Ok(out_output.clone())64}6566impl BuildSystems {67	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {68		let hosts = opts.filter_skipped(config.list_hosts().await?).await?;69		let set = LocalSet::new();70		let build_attr = self.build_attr.clone();71		let batch = (hosts.len() > 1).then(|| {72			config73				.nix_session74				.new_build_batch("build-hosts".to_string())75		});76		for host in hosts {77			let config = config.clone();78			let span = info_span!("build", host = field::display(&host.name));79			let hostname = host.name;80			let build_attr = build_attr.clone();81			let batch = batch.clone();82			set.spawn_local(83				(async move {84					let built = match build_task(config, hostname.clone(), &build_attr, batch).await85					{86						Ok(path) => path,87						Err(e) => {88							error!("failed to deploy host: {}", e);89							return;90						}91					};92					// TODO: Handle error93					let mut out = current_dir().expect("cwd exists");94					out.push(format!("built-{}", hostname));9596					info!("linking iso image to {:?}", out);97					if let Err(e) = symlink(built, out) {98						error!("failed to symlink: {e}")99					}100				})101				.instrument(span),102			);103		}104		drop(batch);105		set.await;106		Ok(())107	}108}109110impl Deploy {111	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {112		let hosts = opts.filter_skipped(config.list_hosts().await?).await?;113		let set = LocalSet::new();114		let batch = (hosts.len() > 1).then(|| {115			config116				.nix_session117				.new_build_batch("deploy-hosts".to_string())118		});119		for host in hosts.into_iter() {120			let config = config.clone();121			let span = info_span!("deploy", host = field::display(&host.name));122			let hostname = host.name.clone();123			let opts = opts.clone();124			let batch = batch.clone();125			if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {126				host.set_deploy_kind(deploy_kind);127			};128			if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {129				host.set_session_destination(destination);130			};131132			set.spawn_local(133				(async move {134					let built =135						match build_task(config.clone(), hostname.clone(), "toplevel", batch).await136						{137							Ok(path) => path,138							Err(e) => {139								error!("failed to build host system closure: {}", e);140								return;141							}142						};143144					let deploy_kind = match host.deploy_kind().await {145						Ok(v) => v,146						Err(e) => {147							error!("failed to query target deploy kind: {e}");148							return;149						}150					};151152					// TODO: Make disable_rollback a host attribute instead153					let mut disable_rollback = self.disable_rollback;154					if !disable_rollback && deploy_kind != DeployKind::Fleet {155						warn!("disabling rollback, as not supported by non-fleet deployment kinds");156						disable_rollback = true;157					}158159					let remote_path =160						match upload_task(&config, &host, GenerationStorage::Deployer, built).await161						{162							Ok(v) => v,163							Err(e) => {164								error!("upload failed: {e}");165								return;166							}167						};168169					if let Err(e) = deploy_task(170						self.action,171						&host,172						remote_path,173						match opts.action_attr(&host, "specialisation").await {174							Ok(v) => v,175							_ => {176								error!("unreachable? failed to get specialization");177								return;178							}179						},180						disable_rollback,181					)182					.await183					{184						error!("activation failed: {e}");185					}186				})187				.instrument(span),188			);189		}190		drop(batch);191		set.await;192		Ok(())193	}194}
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -98,7 +98,9 @@
 	pub name: String,
 	groups: OnceCell<Vec<String>>,
 
+	// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it
 	deploy_kind: OnceCell<DeployKind>,
+	session_destination: OnceCell<String>,
 
 	pub host_config: Option<Value>,
 	pub nixos_config: OnceCell<Value>,
@@ -209,6 +211,12 @@
 		Ok(generations)
 	}
 
+	pub fn set_session_destination(&self, dest: String) {
+		self.session_destination
+			.set(dest)
+			.ok()
+			.expect("session destination is already set")
+	}
 	pub fn set_deploy_kind(&self, kind: DeployKind) {
 		self.deploy_kind
 			.set(kind)
@@ -217,7 +225,7 @@
 	}
 	pub async fn deploy_kind(&self) -> Result<DeployKind> {
 		if let Some(kind) = self.deploy_kind.get() {
-			return Ok(kind.clone());
+			return Ok(*kind);
 		}
 		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {
 			Ok(v) => v,
@@ -237,11 +245,7 @@
 		}
 		// TOCTOU is possible
 		let _ = self.deploy_kind.set(DeployKind::Fleet);
-		Ok(self
-			.deploy_kind
-			.get()
-			.expect("deploy kind is just set")
-			.clone())
+		Ok(*self.deploy_kind.get().expect("deploy kind is just set"))
 	}
 	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {
 		// Prefer sudo, as run0 has some gotchas with polkit
@@ -261,8 +265,10 @@
 			return Ok((*session).clone());
 		};
 		let session = SessionBuilder::default();
+
+		let dest = self.session_destination.get().unwrap_or(&self.name);
 		let session = session
-			.connect(&self.name)
+			.connect(&dest)
 			.await
 			.map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;
 		let session = Arc::new(session);
@@ -281,7 +287,7 @@
 			.arg("test -e \"$1\" && echo true || echo false")
 			.arg("_")
 			.arg(path);
-		Ok(cmd.run_value().await?)
+		cmd.run_value().await
 	}
 	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {
 		let mut cmd = self.cmd("cat").await?;