1use std::{env::current_dir, os::unix::fs::symlink};23use anyhow::Result;4use camino::Utf8PathBuf;5use clap::Parser;6use fleet_base::{7 deploy::{DeployAction, deploy_task, upload_task},8 host::{Config, DeployKind, GenerationStorage},9 opts::FleetOpts,10};11use futures::{StreamExt as _, stream::FuturesUnordered};12use nix_eval::nix_go;13use tokio::task::spawn_blocking;14use tracing::{Instrument, error, field, info, info_span, warn};1516#[derive(Parser)]17pub struct Deploy {18 19 #[clap(long)]20 disable_rollback: bool,21 22 action: DeployAction,23}2425#[derive(Parser, Clone)]26pub struct BuildSystems {27 28 29 #[clap(long, default_value = "toplevel-fleet")]30 build_attr: String,31}3233async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<Utf8PathBuf> {34 info!("building");35 let host = config.host(&hostname)?;36 37 let nixos = host.nixos_config()?;38 let drv = nix_go!(nixos.system.build[{ build_attr }]);39 let out_output = spawn_blocking(move || drv.build("out"))40 .await41 .expect("system derivation build should not panic")?;4243 44 if !host.local {45 info!("adding gc root");46 let local = config.local_host();47 let plugin_id = local.ensure_nix_plugin().await?;48 let nix = local49 .remowt()50 .await?51 .plugin_endpoints::<remowt_fleet::NixClient<_>>(plugin_id);52 let profile = format!(53 "/nix/var/nix/profiles/{}-{hostname}",54 config.data.gc_root_prefix55 );56 nix.switch_profile(profile, out_output.clone())57 .await58 .map_err(|e| anyhow::anyhow!("{e:?}"))?59 .map_err(|e| anyhow::anyhow!("{e}"))?;60 }6162 Ok(out_output)63}6465impl BuildSystems {66 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {67 let hosts = opts.filter_skipped(config.list_hosts()?)?;68 let tasks = FuturesUnordered::new();69 let build_attr = self.build_attr.clone();70 for host in hosts {71 let config = config.clone();72 let span = info_span!("build", host = field::display(&host.name));73 let hostname = host.name;74 let build_attr = build_attr.clone();75 tasks.push(76 (async move {77 let built = match build_task(config, hostname.clone(), &build_attr).await {78 Ok(path) => path,79 Err(e) => {80 error!("failed to deploy host: {:?}", e);81 return;82 }83 };84 85 let mut out = current_dir().expect("cwd exists");86 out.push(format!("built-{hostname}"));8788 info!("linking iso image to {:?}", out);89 if let Err(e) = symlink(built, out) {90 error!("failed to symlink: {e}")91 }92 })93 .instrument(span),94 );95 }96 tasks.collect::<Vec<()>>().await;97 Ok(())98 }99}100101impl Deploy {102 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {103 let hosts = opts.filter_skipped(config.list_hosts()?)?;104 let tasks = FuturesUnordered::new();105 for host in hosts.into_iter() {106 let config = config.clone();107 let span = info_span!("deploy", host = field::display(&host.name));108 let hostname = host.name.clone();109 let opts = opts.clone();110 if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind")? {111 host.set_deploy_kind(deploy_kind);112 };113 if let Some(destination) = opts.action_attr::<String>(&host, "dest")? {114 host.set_session_destination(destination);115 };116 if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store")? {117 host.set_legacy_ssh_store(legacy);118 };119120 tasks.push(121 (async move {122 let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet")123 .await124 {125 Ok(path) => path,126 Err(e) => {127 error!("failed to build host system closure: {:?}", e);128 return;129 }130 };131132 let deploy_kind = match host.deploy_kind().await {133 Ok(v) => v,134 Err(e) => {135 error!("failed to query target deploy kind: {e}");136 return;137 }138 };139140 141 let mut disable_rollback = self.disable_rollback;142 if !disable_rollback && deploy_kind != DeployKind::Fleet {143 warn!("disabling rollback, as not supported by non-fleet deployment kinds");144 disable_rollback = true;145 }146147 let remote_path =148 match upload_task(&config, &host, GenerationStorage::Deployer, built).await149 {150 Ok(v) => v,151 Err(e) => {152 error!("upload failed: {e}");153 return;154 }155 };156157 if let Err(e) = deploy_task(158 self.action,159 &host,160 remote_path,161 match opts.action_attr(&host, "specialisation") {162 Ok(v) => v,163 _ => {164 error!("unreachable? failed to get specialization");165 return;166 }167 },168 disable_rollback,169 )170 .await171 {172 error!("activation failed: {e}");173 }174 })175 .instrument(span),176 );177 }178 tasks.collect::<Vec<()>>().await;179 Ok(())180 }181}