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::{bail, Context, Result};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(65 &self,66 host: &str,67 mut command: MyCommand,68 sudo: bool,69 ) -> Result<String> {70 if sudo {71 command = command.sudo();72 }73 if !self.is_local(host) {74 command = command.ssh(host);75 }76 command.run_string().await77 }7879 pub fn configuration_attr_name(&self, name: &str) -> OsString {80 let mut str = self.directory.as_os_str().to_owned();81 str.push("#");82 str.push(&format!(83 "fleetConfigurations.default.{}.{}",84 self.local_system, name85 ));86 str87 }8889 pub async fn list_hosts(&self) -> Result<Vec<String>> {90 let mut cmd = MyCommand::new("nix");91 cmd.arg("eval")92 .arg(self.configuration_attr_name("configuredHosts"))93 .args(["--apply", "builtins.attrNames", "--json", "--show-trace"])94 .args(&self.nix_args);95 cmd.run_nix_json().await96 }97 pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {98 let mut cmd = MyCommand::new("nix");99 cmd.arg("eval")100 .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))101 .args(["--json", "--show-trace"])102 .args(&self.nix_args);103 cmd.run_nix_json().await104 }105 pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {106 let mut cmd = MyCommand::new("nix");107 cmd.arg("eval")108 .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))109 .args(["--apply", "builtins.attrNames"])110 .args(["--json", "--show-trace"])111 .args(&self.nix_args);112 cmd.run_nix_json().await113 }114 pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {115 let mut cmd = MyCommand::new("nix");116 cmd.arg("eval")117 .arg(118 self.configuration_attr_name(&format!(119 "configuredSystems.{}.config.{}",120 host, attr121 )),122 )123 .args(["--json", "--show-trace"])124 .args(&self.nix_args);125 cmd.run_nix_json().await126 }127128 pub(super) fn data(&self) -> Ref<FleetData> {129 self.data.borrow()130 }131 pub(super) fn data_mut(&self) -> RefMut<FleetData> {132 self.data.borrow_mut()133 }134135 pub fn list_shared(&self) -> Vec<String> {136 let data = self.data();137 data.shared_secrets.keys().cloned().collect()138 }139 pub fn has_shared(&self, name: &str) -> bool {140 let data = self.data();141 data.shared_secrets.contains_key(name)142 }143 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {144 let mut data = self.data_mut();145 data.shared_secrets.insert(name.to_owned(), shared);146 }147 pub fn remove_shared(&self, secret: &str) {148 let mut data = self.data_mut();149 data.shared_secrets.remove(secret);150 }151152 pub fn list_secrets(&self, host: &str) -> Vec<String> {153 let data = self.data();154 let Some(host_secrets) = data.host_secrets.get(host) else {155 return Vec::new();156 };157 host_secrets.keys().cloned().collect()158 }159 pub fn has_secret(&self, host: &str, secret: &str) -> bool {160 let data = self.data();161 let Some(host_secrets) = data.host_secrets.get(host) else {162 return false;163 };164 host_secrets.contains_key(secret)165 }166 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {167 let mut data = self.data_mut();168 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();169 host_secrets.insert(secret, value);170 }171172 pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {173 let data = z85::encode(&data);174 let mut cmd = MyCommand::new("fleet-install-secrets");175 cmd.arg("decrypt").eqarg("--secret", data);176 cmd = cmd.sudo().ssh(host);177 let encoded = cmd178 .run_string()179 .await180 .context("failed to call remote host for decrypt")?181 .trim()182 .to_owned();183 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)184 }185 pub async fn reencrypt_on_host(186 &self,187 host: &str,188 data: Vec<u8>,189 targets: Vec<String>,190 ) -> Result<Vec<u8>> {191 let data = z85::encode(&data);192 let mut recmd = MyCommand::new("fleet-install-secrets");193 recmd.arg("reencrypt").eqarg("--secret", data);194 for target in targets {195 recmd.eqarg("--targets", target);196 }197 recmd = recmd.sudo().ssh(host);198 let encoded = recmd199 .run_string()200 .await201 .context("failed to call remote host for decrypt")?202 .trim()203 .to_owned();204 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)205 }206207 #[must_use]208 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {209 let data = self.data();210 let Some(host_secrets) = data.host_secrets.get(host) else {211 bail!("no secrets for machine {host}");212 };213 let Some(secret) = host_secrets.get(secret) else {214 bail!("machine {host} has no secret {secret}");215 };216 Ok(secret.clone())217 }218 #[must_use]219 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {220 let data = self.data();221 let Some(secret) = data.shared_secrets.get(secret) else {222 bail!("no shared secret {secret}");223 };224 Ok(secret.clone())225 }226227 pub fn save(&self) -> Result<()> {228 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;229 let data = nixlike::serialize(&self.data() as &FleetData)?;230 tempfile.write_all(231 format!(232 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",233 data234 )235 .as_bytes(),236 )?;237 let mut fleet_data_path = self.directory.clone();238 fleet_data_path.push("fleet.nix");239 tempfile.persist(fleet_data_path)?;240 Ok(())241 }242}243244#[derive(Parser, Clone)]245#[clap(group = ArgGroup::new("target_hosts"))]246pub struct FleetOpts {247 248 #[clap(long, number_of_values = 1, group = "target_hosts")]249 only: Vec<String>,250251 252 #[clap(long, number_of_values = 1, group = "target_hosts")]253 skip: Vec<String>,254255 256 #[clap(long)]257 pub localhost: Option<String>,258259 260 261 262 #[clap(long, default_value = "x86_64-linux")]263 pub local_system: String,264}265266impl FleetOpts {267 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {268 let local_system = self.local_system.clone();269 if self.localhost.is_none() {270 self.localhost271 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());272 }273 let directory = current_dir()?;274275 let mut fleet_data_path = directory.clone();276 fleet_data_path.push("fleet.nix");277 let bytes = std::fs::read_to_string(fleet_data_path)?;278 let data = nixlike::parse_str(&bytes)?;279280 Ok(Config(Arc::new(FleetConfigInternals {281 opts: self,282 directory,283 data,284 local_system,285 nix_args,286 })))287 }288}