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 17 #[clap(long)]18 disable_rollback: bool,19 20 action: DeployAction,21}2223#[derive(Parser, Clone)]24pub struct BuildSystems {25 26 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 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 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 93 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 153 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}