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, bail, Context};12use clap::{ArgGroup, Parser};13use serde::de::DeserializeOwned;14use tempfile::NamedTempFile;15use tokio::process::Command;1617use crate::{18 command::CommandExt,19 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},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(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 encoded = self.command_on(host, "fleet-install-secrets", true)175 .arg("decrypt")176 .arg("--secret")177 .arg(data).run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();178 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)179 }180 pub async fn reencrypt_on_host(&self, host: &str, data: Vec<u8>, targets: Vec<String>) -> Result<Vec<u8>>{181 let data = z85::encode(&data);182 let mut recmd = self.command_on(host, "fleet-install-secrets", true);183 recmd184 .arg("reencrypt")185 .arg("--secret")186 .arg(format!("\"{}\"", data.replace('$', "\\$")));187 for target in targets {188 recmd.arg("--targets");189 recmd.arg(format!("\"{target}\""));190 }191 let encoded = recmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();192 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)193 }194195 #[must_use]196 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {197 let data = self.data();198 let Some(host_secrets) = data.host_secrets.get(host) else {199 bail!("no secrets for machine {host}");200 };201 let Some(secret) = host_secrets.get(secret) else {202 bail!("machine {host} has no secret {secret}");203 };204 Ok(secret.clone())205 }206 #[must_use]207 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {208 let data = self.data();209 let Some(secret) = data.shared_secrets.get(secret) else {210 bail!("no shared secret {secret}");211 };212 Ok(secret.clone())213 }214215 pub fn save(&self) -> Result<()> {216 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;217 let data = nixlike::serialize(&self.data() as &FleetData)?;218 tempfile.write_all(219 format!(220 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",221 data222 )223 .as_bytes(),224 )?;225 let mut fleet_data_path = self.directory.clone();226 fleet_data_path.push("fleet.nix");227 tempfile.persist(fleet_data_path)?;228 Ok(())229 }230}231232#[derive(Parser, Clone)]233#[clap(group = ArgGroup::new("target_hosts"))]234pub struct FleetOpts {235 236 #[clap(long, number_of_values = 1, group = "target_hosts")]237 only: Vec<String>,238239 240 #[clap(long, number_of_values = 1, group = "target_hosts")]241 skip: Vec<String>,242243 244 #[clap(long)]245 pub localhost: Option<String>,246247 248 249 250 #[clap(long, default_value = "x86_64-linux")]251 pub local_system: String,252}253254impl FleetOpts {255 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {256 let local_system = self.local_system.clone();257 if self.localhost.is_none() {258 self.localhost259 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());260 }261 let directory = current_dir()?;262263 let mut fleet_data_path = directory.clone();264 fleet_data_path.push("fleet.nix");265 let bytes = std::fs::read_to_string(fleet_data_path)?;266 let data = nixlike::parse_str(&bytes)?;267268 Ok(Config(Arc::new(FleetConfigInternals {269 opts: self,270 directory,271 data,272 local_system,273 nix_args,274 })))275 }276}