1use std::{2 cell::{Ref, RefCell, RefMut},3 env::current_dir,4 ffi::{OsStr, OsString},5 ops::Deref,6 path::PathBuf,7 sync::Arc,8};910use anyhow::Result;11use clap::{ArgGroup, Parser};12use serde::de::DeserializeOwned;13use tokio::process::Command;1415use crate::{command::CommandExt, fleetdata::FleetData};1617pub struct FleetConfigInternals {18 pub local_system: String,19 pub directory: PathBuf,20 pub opts: FleetOpts,21 pub data: RefCell<FleetData>,22}2324#[derive(Clone)]25pub struct Config(Arc<FleetConfigInternals>);2627impl Deref for Config {28 type Target = FleetConfigInternals;2930 fn deref(&self) -> &Self::Target {31 &self.032 }33}3435impl Config {36 pub fn should_skip(&self, host: &str) -> bool {37 if !self.opts.skip.is_empty() {38 self.opts.skip.iter().any(|h| h as &str == host)39 } else if !self.opts.only.is_empty() {40 !self.opts.only.iter().any(|h| h as &str == host)41 } else {42 false43 }44 }45 pub fn is_local(&self, host: &str) -> bool {46 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)47 }4849 pub fn command_on(&self, host: &str, program: impl AsRef<OsStr>, sudo: bool) -> Command {50 if self.is_local(host) {51 if sudo {52 let mut cmd = Command::new("sudo");53 cmd.arg(program);54 cmd55 } else {56 Command::new(program)57 }58 } else {59 let mut cmd = Command::new("ssh");60 cmd.arg(host).arg("--");61 if sudo {62 cmd.arg("sudo");63 }64 cmd.arg(program);65 cmd66 }67 }6869 pub fn configuration_attr_name(&self, name: &str) -> OsString {70 let mut str = self.directory.as_os_str().to_owned();71 str.push("#");72 str.push(&format!(73 "fleetConfigurations.default.{}.{}",74 self.local_system, name75 ));76 str77 }7879 pub async fn list_hosts(&self) -> Result<Vec<String>> {80 Command::new("nix")81 .arg("eval")82 .arg(self.configuration_attr_name("configuredHosts"))83 .args(&["--apply", "builtins.attrNames", "--json", "--show-trace"])84 .run_nix_json()85 .await86 }87 pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {88 Command::new("nix")89 .arg("eval")90 .arg(91 self.configuration_attr_name(&format!(92 "configuredSystems.{}.config.{}",93 host, attr94 )),95 )96 .args(&["--json", "--show-trace"])97 .run_nix_json()98 .await99 }100101 pub fn data(&self) -> Ref<FleetData> {102 self.data.borrow()103 }104 pub fn data_mut(&self) -> RefMut<FleetData> {105 self.data.borrow_mut()106 }107108 pub fn save(&self) -> Result<()> {109 let mut fleet_data_path = self.directory.clone();110 fleet_data_path.push("fleet.nix");111 let data = nixlike::serialize(&self.data() as &FleetData)?;112 std::fs::write(113 fleet_data_path,114 format!(115 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",116 data117 ),118 )?;119 Ok(())120 }121}122123#[derive(Parser, Clone)]124#[clap(group = ArgGroup::new("target_hosts"))]125pub struct FleetOpts {126 127 #[clap(long, number_of_values = 1, group = "target_hosts")]128 only: Vec<String>,129130 131 #[clap(long, number_of_values = 1, group = "target_hosts")]132 skip: Vec<String>,133134 135 #[clap(long)]136 pub localhost: Option<String>,137138 139 140 141 #[clap(long, default_value = "x86_64-linux")]142 pub local_system: String,143}144145impl FleetOpts {146 pub fn build(mut self) -> Result<Config> {147 let local_system = self.local_system.clone();148 if self.localhost.is_none() {149 self.localhost150 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());151 }152 let directory = current_dir()?;153154 let mut fleet_data_path = directory.clone();155 fleet_data_path.push("fleet.nix");156 let bytes = std::fs::read_to_string(fleet_data_path)?;157 let data = nixlike::parse_str(&bytes)?;158159 Ok(Config(Arc::new(FleetConfigInternals {160 opts: self,161 directory,162 data,163 local_system,164 })))165 }166}