git.delta.rocks / jrsonnet / refs/commits / 7e2e5c591e04

difftreelog

source

cmds/fleet/src/host.rs7.9 KiBsourcehistory
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::{anyhow, bail, Context, Result};11use clap::{ArgGroup, Parser};12use openssh::SessionBuilder;13use tempfile::NamedTempFile;1415use crate::{16	better_nix_eval::{Field, NixSessionPool},17	command::MyCommand,18	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},19	nix_go, nix_go_json,20};2122pub struct FleetConfigInternals {23	pub local_system: String,24	pub directory: PathBuf,25	pub opts: FleetOpts,26	pub data: Mutex<FleetData>,27	pub nix_args: Vec<OsString>,28	/// fleetConfigurations.<name>.<localSystem>29	pub fleet_field: Field,30	/// fleet_config.configUnchecked31	pub config_field: Field,32	/// fleet_config.unchecked33	pub config_unchecked_field: Field,34}3536#[derive(Clone)]37pub struct Config(Arc<FleetConfigInternals>);3839impl Deref for Config {40	type Target = FleetConfigInternals;4142	fn deref(&self) -> &Self::Target {43		&self.044	}45}4647pub struct ConfigHost {48	pub name: String,49}50impl ConfigHost {51	async fn open_session(&self) -> Result<openssh::Session> {52		let mut session = SessionBuilder::default();5354		session55			.connect(&self.name)56			.await57			.map_err(|e| anyhow!("ssh error: {e}"))58	}59}6061impl Config {62	pub fn should_skip(&self, host: &str) -> bool {63		if !self.opts.skip.is_empty() {64			self.opts.skip.iter().any(|h| h as &str == host)65		} else if !self.opts.only.is_empty() {66			!self.opts.only.iter().any(|h| h as &str == host)67		} else {68			false69		}70	}71	pub fn is_local(&self, host: &str) -> bool {72		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)73	}7475	pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {76		if sudo {77			command = command.sudo();78		}79		if !self.is_local(host) {80			command = command.ssh(host);81		}82		command.run().await83	}84	pub async fn run_string_on(85		&self,86		host: &str,87		mut command: MyCommand,88		sudo: bool,89	) -> Result<String> {90		if sudo {91			command = command.sudo();92		}93		if !self.is_local(host) {94			command = command.ssh(host);95		}96		command.run_string().await97	}9899	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {100		let fleet_field = &self.fleet_field;101		let names = nix_go!(fleet_field.configuredHosts).list_fields().await?;102		let mut out = vec![];103		for name in names {104			out.push(ConfigHost { name })105		}106		Ok(out)107	}108	pub async fn system_config(&self, host: &str) -> Result<Field> {109		let fleet_field = &self.fleet_field;110		Ok(nix_go!(fleet_field.configuredSystems[{ host }].config))111	}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	/// Shared secrets configured in fleet.nix or in flake120	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {121		let config_field = &self.config_field;122		nix_go!(config_field.sharedSecrets).list_fields().await123	}124	/// Shared secrets configured in fleet.nix125	pub fn list_shared(&self) -> Vec<String> {126		let data = self.data();127		data.shared_secrets.keys().cloned().collect()128	}129	pub fn has_shared(&self, name: &str) -> bool {130		let data = self.data();131		data.shared_secrets.contains_key(name)132	}133	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {134		let mut data = self.data_mut();135		data.shared_secrets.insert(name.to_owned(), shared);136	}137	pub fn remove_shared(&self, secret: &str) {138		let mut data = self.data_mut();139		data.shared_secrets.remove(secret);140	}141142	pub fn has_secret(&self, host: &str, secret: &str) -> bool {143		let data = self.data();144		let Some(host_secrets) = data.host_secrets.get(host) else {145			return false;146		};147		host_secrets.contains_key(secret)148	}149	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {150		let mut data = self.data_mut();151		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();152		host_secrets.insert(secret, value);153	}154155	pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {156		let data = z85::encode(&data);157		let mut cmd = MyCommand::new("fleet-install-secrets");158		cmd.arg("decrypt").eqarg("--secret", data);159		cmd = cmd.sudo().ssh(host);160		let encoded = cmd161			.run_string()162			.await163			.context("failed to call remote host for decrypt")?164			.trim()165			.to_owned();166		z85::decode(encoded).context("bad encoded data? outdated host?")167	}168	pub async fn reencrypt_on_host(169		&self,170		host: &str,171		data: Vec<u8>,172		targets: Vec<String>,173	) -> Result<Vec<u8>> {174		let data = z85::encode(&data);175		let mut recmd = MyCommand::new("fleet-install-secrets");176		recmd.arg("reencrypt").eqarg("--secret", data);177		for target in targets {178			recmd.eqarg("--targets", target);179		}180		recmd = recmd.sudo().ssh(host);181		let encoded = recmd182			.run_string()183			.await184			.context("failed to call remote host for decrypt")?185			.trim()186			.to_owned();187		z85::decode(encoded).context("bad encoded data? outdated host?")188	}189190	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {191		let data = self.data();192		let Some(host_secrets) = data.host_secrets.get(host) else {193			bail!("no secrets for machine {host}");194		};195		let Some(secret) = host_secrets.get(secret) else {196			bail!("machine {host} has no secret {secret}");197		};198		Ok(secret.clone())199	}200	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {201		let data = self.data();202		let Some(secret) = data.shared_secrets.get(secret) else {203			bail!("no shared secret {secret}");204		};205		Ok(secret.clone())206	}207	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {208		let config_field = &self.config_field;209		Ok(nix_go_json!(210			config_field.sharedSecrets[{ secret }].expectedOwners211		))212	}213214	pub fn save(&self) -> Result<()> {215		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;216		let data = nixlike::serialize(&self.data() as &FleetData)?;217		tempfile.write_all(218			format!(219				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",220				data221			)222			.as_bytes(),223		)?;224		let mut fleet_data_path = self.directory.clone();225		fleet_data_path.push("fleet.nix");226		tempfile.persist(fleet_data_path)?;227		Ok(())228	}229}230231#[derive(Parser, Clone)]232#[clap(group = ArgGroup::new("target_hosts"))]233pub struct FleetOpts {234	/// All hosts except those would be skipped235	#[clap(long, number_of_values = 1, group = "target_hosts")]236	only: Vec<String>,237238	/// Hosts to skip239	#[clap(long, number_of_values = 1, group = "target_hosts")]240	skip: Vec<String>,241242	/// Host, which should be threaten as current machine243	#[clap(long)]244	pub localhost: Option<String>,245246	/// Override detected system for host, to perform builds via247	/// binfmt-declared qemu instead of trying to crosscompile248	#[clap(long, default_value = "detect")]249	pub local_system: String,250}251252impl FleetOpts {253	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {254		if self.localhost.is_none() {255			self.localhost256				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());257		}258		let directory = current_dir()?;259260		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;261		let root_field = pool.get().await?;262263		if self.local_system == "detect" {264			let builtins_field = Field::field(root_field.clone(), "builtins").await?;265			self.local_system = nix_go_json!(builtins_field.currentSystem);266		}267		let local_system = self.local_system.clone();268269		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;270271		let fleet_field = nix_go!(fleet_root.default);272		let config_field = nix_go!(fleet_field.configUnchecked);273		let config_unchecked_field = nix_go!(fleet_field.unchecked);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			fleet_field,287			config_field,288			config_unchecked_field,289		})))290	}291}