1use std::{2 env::current_dir,3 ffi::OsString,4 io::Write,5 ops::Deref,6 path::PathBuf,7 sync::{Arc, Mutex, MutexGuard},8};910use anyhow::{bail, Context, Result};11use clap::{ArgGroup, Parser};12use tempfile::NamedTempFile;1314use crate::{15 better_nix_eval::{Field, NixSessionPool},16 command::MyCommand,17 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},18};1920pub struct FleetConfigInternals {21 pub local_system: String,22 pub directory: PathBuf,23 pub opts: FleetOpts,24 pub data: Mutex<FleetData>,25 pub nix_args: Vec<OsString>,26 27 pub fleet_field: Field,28 29 pub config_field: Field,30}3132#[derive(Clone)]33pub struct Config(Arc<FleetConfigInternals>);3435impl Deref for Config {36 type Target = FleetConfigInternals;3738 fn deref(&self) -> &Self::Target {39 &self.040 }41}4243pub struct ConfigHost {44 pub name: String,45}4647impl Config {48 pub fn should_skip(&self, host: &str) -> bool {49 if !self.opts.skip.is_empty() {50 self.opts.skip.iter().any(|h| h as &str == host)51 } else if !self.opts.only.is_empty() {52 !self.opts.only.iter().any(|h| h as &str == host)53 } else {54 false55 }56 }57 pub fn is_local(&self, host: &str) -> bool {58 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)59 }6061 pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {62 if sudo {63 command = command.sudo();64 }65 if !self.is_local(host) {66 command = command.ssh(host);67 }68 command.run().await69 }70 pub async fn run_string_on(71 &self,72 host: &str,73 mut command: MyCommand,74 sudo: bool,75 ) -> Result<String> {76 if sudo {77 command = command.sudo();78 }79 if !self.is_local(host) {80 command = command.ssh(host);81 }82 command.run_string().await83 }8485 pub fn configuration_attr_name(&self, name: &str) -> OsString {86 let mut str = self.directory.as_os_str().to_owned();87 str.push("#");88 str.push(&format!(89 "fleetConfigurations.default.{}.{}",90 self.local_system, name91 ));92 str93 }9495 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {96 let names = self.fleet_field97 .get_field_deep(["configuredHosts"])98 .await?99 .list_fields()100 .await?;101 let mut out = vec![];102 for name in names {103 out.push(ConfigHost {104 name,105 })106 }107 Ok(out)108 }109 pub async fn system_config(&self, host: &str) -> Result<Field> {110 self.fleet_field.get_field_deep(["configuredSystems", host, "config"]).await111 }112113 pub(super) fn data(&self) -> MutexGuard<FleetData> {114 self.data.lock().unwrap()115 }116 pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {117 self.data.lock().unwrap()118 }119 120 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {121 self.config_field122 .get_field("sharedSecrets")123 .await?124 .list_fields()125 .await126 }127 128 pub fn list_shared(&self) -> Vec<String> {129 let data = self.data();130 data.shared_secrets.keys().cloned().collect()131 }132 pub fn has_shared(&self, name: &str) -> bool {133 let data = self.data();134 data.shared_secrets.contains_key(name)135 }136 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {137 let mut data = self.data_mut();138 data.shared_secrets.insert(name.to_owned(), shared);139 }140 pub fn remove_shared(&self, secret: &str) {141 let mut data = self.data_mut();142 data.shared_secrets.remove(secret);143 }144145 pub fn has_secret(&self, host: &str, secret: &str) -> bool {146 let data = self.data();147 let Some(host_secrets) = data.host_secrets.get(host) else {148 return false;149 };150 host_secrets.contains_key(secret)151 }152 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {153 let mut data = self.data_mut();154 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();155 host_secrets.insert(secret, value);156 }157158 pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {159 let data = z85::encode(&data);160 let mut cmd = MyCommand::new("fleet-install-secrets");161 cmd.arg("decrypt").eqarg("--secret", data);162 cmd = cmd.sudo().ssh(host);163 let encoded = cmd164 .run_string()165 .await166 .context("failed to call remote host for decrypt")?167 .trim()168 .to_owned();169 z85::decode(encoded).context("bad encoded data? outdated host?")170 }171 pub async fn reencrypt_on_host(172 &self,173 host: &str,174 data: Vec<u8>,175 targets: Vec<String>,176 ) -> Result<Vec<u8>> {177 let data = z85::encode(&data);178 let mut recmd = MyCommand::new("fleet-install-secrets");179 recmd.arg("reencrypt").eqarg("--secret", data);180 for target in targets {181 recmd.eqarg("--targets", target);182 }183 recmd = recmd.sudo().ssh(host);184 let encoded = recmd185 .run_string()186 .await187 .context("failed to call remote host for decrypt")?188 .trim()189 .to_owned();190 z85::decode(encoded).context("bad encoded data? outdated host?")191 }192193 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {194 let data = self.data();195 let Some(host_secrets) = data.host_secrets.get(host) else {196 bail!("no secrets for machine {host}");197 };198 let Some(secret) = host_secrets.get(secret) else {199 bail!("machine {host} has no secret {secret}");200 };201 Ok(secret.clone())202 }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 }210 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {211 self.config_field212 .get_field_deep(["sharedSecrets", secret, "expectedOwners"])213 .await?214 .as_json()215 .await216 }217218 pub fn save(&self) -> Result<()> {219 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;220 let data = nixlike::serialize(&self.data() as &FleetData)?;221 tempfile.write_all(222 format!(223 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",224 data225 )226 .as_bytes(),227 )?;228 let mut fleet_data_path = self.directory.clone();229 fleet_data_path.push("fleet.nix");230 tempfile.persist(fleet_data_path)?;231 Ok(())232 }233}234235#[derive(Parser, Clone)]236#[clap(group = ArgGroup::new("target_hosts"))]237pub struct FleetOpts {238 239 #[clap(long, number_of_values = 1, group = "target_hosts")]240 only: Vec<String>,241242 243 #[clap(long, number_of_values = 1, group = "target_hosts")]244 skip: Vec<String>,245246 247 #[clap(long)]248 pub localhost: Option<String>,249250 251 252 253 #[clap(long, default_value = "detect")]254 pub local_system: String,255}256257impl FleetOpts {258 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {259 if self.localhost.is_none() {260 self.localhost261 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());262 }263 let directory = current_dir()?;264265 let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;266 let root_field = pool.get().await?;267268 if self.local_system == "detect" {269 let builtins_field = Field::field(root_field.clone(), "builtins").await?;270 let system = builtins_field.get_field("currentSystem").await?;271 self.local_system = system.as_json().await?;272 }273 let local_system = self.local_system.clone();274275 let fleet_root = Field::field(root_field, "fleetConfigurations").await?;276277 let fleet_field = fleet_root278 .get_field_deep(["default", &local_system])279 .await?;280 let config_field = fleet_field.get_field("configUnchecked").await?;281282 let mut fleet_data_path = directory.clone();283 fleet_data_path.push("fleet.nix");284 let bytes = std::fs::read_to_string(fleet_data_path)?;285 let data = nixlike::parse_str(&bytes)?;286287 Ok(Config(Arc::new(FleetConfigInternals {288 opts: self,289 directory,290 data,291 local_system,292 nix_args,293 fleet_field,294 config_field,295 })))296 }297}