1use std::{2 cell::{Ref, RefCell, RefMut},3 env::current_dir,4 ffi::{OsStr, OsString},5 io::Write,6 ops::Deref,7 path::PathBuf,8 sync::Arc,9};1011use anyhow::Result;12use clap::{ArgGroup, Parser};13use serde::de::DeserializeOwned;14use tempfile::{NamedTempFile, TempDir};15use tokio::process::Command;1617use crate::{18 command::CommandExt,19 fleetdata::{dummy_flake, FleetData},20};2122pub struct FleetConfigInternals {23 pub local_system: String,24 pub directory: PathBuf,25 pub opts: FleetOpts,26 pub data: RefCell<FleetData>,27 pub nix_args: Vec<OsString>,28}2930#[derive(Clone)]31pub struct Config(Arc<FleetConfigInternals>);3233impl Deref for Config {34 type Target = FleetConfigInternals;3536 fn deref(&self) -> &Self::Target {37 &self.038 }39}4041impl Config {42 pub fn should_skip(&self, host: &str) -> bool {43 if !self.opts.skip.is_empty() {44 self.opts.skip.iter().any(|h| h as &str == host)45 } else if !self.opts.only.is_empty() {46 !self.opts.only.iter().any(|h| h as &str == host)47 } else {48 false49 }50 }51 pub fn is_local(&self, host: &str) -> bool {52 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)53 }5455 pub fn command_on(&self, host: &str, program: impl AsRef<OsStr>, sudo: bool) -> Command {56 if self.is_local(host) {57 if sudo {58 let mut cmd = Command::new("sudo");59 cmd.arg(program);60 cmd61 } else {62 Command::new(program)63 }64 } else {65 let mut cmd = Command::new("ssh");66 cmd.arg(host).arg("--");67 if sudo {68 cmd.arg("sudo");69 }70 cmd.arg(program);71 cmd72 }73 }7475 pub fn configuration_attr_name(&self, name: &str) -> OsString {76 let mut str = self.directory.as_os_str().to_owned();77 str.push("#");78 str.push(&format!(79 "fleetConfigurations.default.{}.{}",80 self.local_system, name81 ));82 str83 }8485 pub async fn list_hosts(&self) -> Result<Vec<String>> {86 Command::new("nix")87 .arg("eval")88 .arg(self.configuration_attr_name("configuredHosts"))89 .args(["--apply", "builtins.attrNames", "--json", "--show-trace"])90 .args(&self.nix_args)91 .run_nix_json()92 .await93 }94 pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {95 Command::new("nix")96 .arg("eval")97 .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))98 .args(["--json", "--show-trace"])99 .args(&self.nix_args)100 .run_nix_json()101 .await102 }103 pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {104 Command::new("nix")105 .arg("eval")106 .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))107 .args(["--apply", "builtins.attrNames"])108 .args(["--json", "--show-trace"])109 .args(&self.nix_args)110 .run_nix_json()111 .await112 }113 pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {114 Command::new("nix")115 .arg("eval")116 .arg(117 self.configuration_attr_name(&format!(118 "configuredSystems.{}.config.{}",119 host, attr120 )),121 )122 .args(["--json", "--show-trace"])123 .args(&self.nix_args)124 .run_nix_json()125 .await126 }127128 pub fn data(&self) -> Ref<FleetData> {129 self.data.borrow()130 }131 pub fn data_mut(&self) -> RefMut<FleetData> {132 self.data.borrow_mut()133 }134135 pub fn save(&self) -> Result<()> {136 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;137 let data = nixlike::serialize(&self.data() as &FleetData)?;138 tempfile.write_all(139 format!(140 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",141 data142 )143 .as_bytes(),144 )?;145 let mut fleet_data_path = self.directory.clone();146 fleet_data_path.push("fleet.nix");147 tempfile.persist(fleet_data_path)?;148 Ok(())149 }150}151152#[derive(Parser, Clone)]153#[clap(group = ArgGroup::new("target_hosts"))]154pub struct FleetOpts {155 156 #[clap(long, number_of_values = 1, group = "target_hosts")]157 only: Vec<String>,158159 160 #[clap(long, number_of_values = 1, group = "target_hosts")]161 skip: Vec<String>,162163 164 #[clap(long)]165 pub localhost: Option<String>,166167 168 169 170 #[clap(long, default_value = "x86_64-linux")]171 pub local_system: String,172}173174impl FleetOpts {175 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {176 let local_system = self.local_system.clone();177 if self.localhost.is_none() {178 self.localhost179 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());180 }181 let directory = current_dir()?;182183 let mut fleet_data_path = directory.clone();184 fleet_data_path.push("fleet.nix");185 let bytes = std::fs::read_to_string(fleet_data_path)?;186 let data = nixlike::parse_str(&bytes)?;187188 Ok(Config(Arc::new(FleetConfigInternals {189 opts: self,190 directory,191 data,192 local_system,193 nix_args,194 })))195 }196}