1use std::{2 cell::{Ref, RefCell, RefMut},3 env::current_dir,4 ffi::OsString,5 io::Write,6 ops::Deref,7 path::PathBuf,8 sync::Arc,9};1011use anyhow::{Result, bail, Context};12use clap::{ArgGroup, Parser};13use serde::de::DeserializeOwned;14use tempfile::NamedTempFile;1516use crate::{17 command::MyCommand,18 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},19};2021pub struct FleetConfigInternals {22 pub local_system: String,23 pub directory: PathBuf,24 pub opts: FleetOpts,25 pub data: RefCell<FleetData>,26 pub nix_args: Vec<OsString>,27}2829#[derive(Clone)]30pub struct Config(Arc<FleetConfigInternals>);3132impl Deref for Config {33 type Target = FleetConfigInternals;3435 fn deref(&self) -> &Self::Target {36 &self.037 }38}3940impl Config {41 pub fn should_skip(&self, host: &str) -> bool {42 if !self.opts.skip.is_empty() {43 self.opts.skip.iter().any(|h| h as &str == host)44 } else if !self.opts.only.is_empty() {45 !self.opts.only.iter().any(|h| h as &str == host)46 } else {47 false48 }49 }50 pub fn is_local(&self, host: &str) -> bool {51 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)52 }5354 pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {55 if sudo {56 command = command.sudo();57 }58 if !self.is_local(host) {59 command = command.ssh(host);60 }61 command.run().await62 }63 #[must_use]64 pub async fn run_string_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<String> {65 if sudo {66 command = command.sudo();67 }68 if !self.is_local(host) {69 command = command.ssh(host);70 }71 command.run_string().await72 }7374 pub fn configuration_attr_name(&self, name: &str) -> OsString {75 let mut str = self.directory.as_os_str().to_owned();76 str.push("#");77 str.push(&format!(78 "fleetConfigurations.default.{}.{}",79 self.local_system, name80 ));81 str82 }8384 pub async fn list_hosts(&self) -> Result<Vec<String>> {85 let mut cmd = MyCommand::new("nix");86 cmd.arg("eval")87 .arg(self.configuration_attr_name("configuredHosts"))88 .args(["--apply", "builtins.attrNames", "--json", "--show-trace"])89 .args(&self.nix_args);90 cmd.run_nix_json()91 .await92 }93 pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {94 let mut cmd = MyCommand::new("nix");95 cmd.arg("eval")96 .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))97 .args(["--json", "--show-trace"])98 .args(&self.nix_args);99 cmd.run_nix_json()100 .await101 }102 pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {103 let mut cmd = MyCommand::new("nix");104 cmd.arg("eval")105 .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))106 .args(["--apply", "builtins.attrNames"])107 .args(["--json", "--show-trace"])108 .args(&self.nix_args);109 cmd.run_nix_json()110 .await111 }112 pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {113 let mut cmd = MyCommand::new("nix");114 cmd.arg("eval")115 .arg(116 self.configuration_attr_name(&format!(117 "configuredSystems.{}.config.{}",118 host, attr119 )),120 )121 .args(["--json", "--show-trace"])122 .args(&self.nix_args);123 cmd.run_nix_json()124 .await125 }126127 pub(super) fn data(&self) -> Ref<FleetData> {128 self.data.borrow()129 }130 pub(super) fn data_mut(&self) -> RefMut<FleetData> {131 self.data.borrow_mut()132 }133134 pub fn list_shared(&self) -> Vec<String> {135 let data = self.data();136 data.shared_secrets.keys().cloned().collect()137 }138 pub fn has_shared(&self, name: &str) -> bool {139 let data = self.data();140 data.shared_secrets.contains_key(name)141 }142 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {143 let mut data = self.data_mut();144 data.shared_secrets.insert(name.to_owned(), shared);145 }146 pub fn remove_shared(&self, secret: &str) {147 let mut data = self.data_mut();148 data.shared_secrets.remove(secret);149 }150151 pub fn list_secrets(&self, host: &str) -> Vec<String> {152 let data = self.data();153 let Some(host_secrets) = data.host_secrets.get(host) else {154 return Vec::new(); 155 };156 host_secrets.keys().cloned().collect()157 }158 pub fn has_secret(&self, host: &str, secret: &str) -> bool {159 let data = self.data();160 let Some(host_secrets) = data.host_secrets.get(host) else {161 return false; 162 };163 host_secrets.contains_key(secret)164 }165 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {166 let mut data = self.data_mut();167 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();168 host_secrets.insert(secret, value);169 }170171 pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>>{172 let data = z85::encode(&data);173 let mut cmd = MyCommand::new("fleet-install-secrets");174 cmd.arg("decrypt").eqarg("--secret", data);175 cmd = cmd.sudo().ssh(host);176 let encoded = cmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();177 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)178 }179 pub async fn reencrypt_on_host(&self, host: &str, data: Vec<u8>, targets: Vec<String>) -> Result<Vec<u8>>{180 let data = z85::encode(&data);181 let mut recmd = MyCommand::new("fleet-install-secrets");182 recmd.arg("reencrypt").eqarg("--secret",data);183 for target in targets {184 recmd.eqarg("--targets", target);185 }186 recmd = recmd.sudo().ssh(host);187 let encoded = recmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();188 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)189 }190191 #[must_use]192 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {193 let data = self.data();194 let Some(host_secrets) = data.host_secrets.get(host) else {195 bail!("no secrets for machine {host}");196 };197 let Some(secret) = host_secrets.get(secret) else {198 bail!("machine {host} has no secret {secret}");199 };200 Ok(secret.clone())201 }202 #[must_use]203 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {204 let data = self.data();205 let Some(secret) = data.shared_secrets.get(secret) else {206 bail!("no shared secret {secret}");207 };208 Ok(secret.clone())209 }210211 pub fn save(&self) -> Result<()> {212 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;213 let data = nixlike::serialize(&self.data() as &FleetData)?;214 tempfile.write_all(215 format!(216 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",217 data218 )219 .as_bytes(),220 )?;221 let mut fleet_data_path = self.directory.clone();222 fleet_data_path.push("fleet.nix");223 tempfile.persist(fleet_data_path)?;224 Ok(())225 }226}227228#[derive(Parser, Clone)]229#[clap(group = ArgGroup::new("target_hosts"))]230pub struct FleetOpts {231 232 #[clap(long, number_of_values = 1, group = "target_hosts")]233 only: Vec<String>,234235 236 #[clap(long, number_of_values = 1, group = "target_hosts")]237 skip: Vec<String>,238239 240 #[clap(long)]241 pub localhost: Option<String>,242243 244 245 246 #[clap(long, default_value = "x86_64-linux")]247 pub local_system: String,248}249250impl FleetOpts {251 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {252 let local_system = self.local_system.clone();253 if self.localhost.is_none() {254 self.localhost255 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());256 }257 let directory = current_dir()?;258259 let mut fleet_data_path = directory.clone();260 fleet_data_path.push("fleet.nix");261 let bytes = std::fs::read_to_string(fleet_data_path)?;262 let data = nixlike::parse_str(&bytes)?;263264 Ok(Config(Arc::new(FleetConfigInternals {265 opts: self,266 directory,267 data,268 local_system,269 nix_args,270 })))271 }272}