git.delta.rocks / jrsonnet / refs/commits / 89d35672dcfd

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, Index, NixSessionPool},17	command::MyCommand,18	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},19	nix_path,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}3334#[derive(Clone)]35pub struct Config(Arc<FleetConfigInternals>);3637impl Deref for Config {38	type Target = FleetConfigInternals;3940	fn deref(&self) -> &Self::Target {41		&self.042	}43}4445pub struct ConfigHost {46	pub name: String,47}48impl ConfigHost {49	async fn open_session(&self) -> Result<openssh::Session> {50		let mut session = SessionBuilder::default();5152		session53			.connect(&self.name)54			.await55			.map_err(|e| anyhow!("ssh error: {e}"))56	}57}5859impl Config {60	pub fn should_skip(&self, host: &str) -> bool {61		if !self.opts.skip.is_empty() {62			self.opts.skip.iter().any(|h| h as &str == host)63		} else if !self.opts.only.is_empty() {64			!self.opts.only.iter().any(|h| h as &str == host)65		} else {66			false67		}68	}69	pub fn is_local(&self, host: &str) -> bool {70		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)71	}7273	pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {74		if sudo {75			command = command.sudo();76		}77		if !self.is_local(host) {78			command = command.ssh(host);79		}80		command.run().await81	}82	pub async fn run_string_on(83		&self,84		host: &str,85		mut command: MyCommand,86		sudo: bool,87	) -> Result<String> {88		if sudo {89			command = command.sudo();90		}91		if !self.is_local(host) {92			command = command.ssh(host);93		}94		command.run_string().await95	}9697	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {98		let names = self99			.fleet_field100			.select(nix_path!(.configuredHosts))101			.await?102			.list_fields()103			.await?;104		let mut out = vec![];105		for name in names {106			out.push(ConfigHost { name })107		}108		Ok(out)109	}110	pub async fn system_config(&self, host: &str) -> Result<Field> {111		self.fleet_field112			.select(nix_path!(.configuredSystems.{host}.config))113			.await114	}115116	pub(super) fn data(&self) -> MutexGuard<FleetData> {117		self.data.lock().unwrap()118	}119	pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {120		self.data.lock().unwrap()121	}122	/// Shared secrets configured in fleet.nix or in flake123	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {124		self.config_field125			.select(nix_path!(.sharedSecrets))126			.await?127			.list_fields()128			.await129	}130	/// Shared secrets configured in fleet.nix131	pub fn list_shared(&self) -> Vec<String> {132		let data = self.data();133		data.shared_secrets.keys().cloned().collect()134	}135	pub fn has_shared(&self, name: &str) -> bool {136		let data = self.data();137		data.shared_secrets.contains_key(name)138	}139	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {140		let mut data = self.data_mut();141		data.shared_secrets.insert(name.to_owned(), shared);142	}143	pub fn remove_shared(&self, secret: &str) {144		let mut data = self.data_mut();145		data.shared_secrets.remove(secret);146	}147148	pub fn has_secret(&self, host: &str, secret: &str) -> bool {149		let data = self.data();150		let Some(host_secrets) = data.host_secrets.get(host) else {151			return false;152		};153		host_secrets.contains_key(secret)154	}155	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {156		let mut data = self.data_mut();157		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();158		host_secrets.insert(secret, value);159	}160161	pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {162		let data = z85::encode(&data);163		let mut cmd = MyCommand::new("fleet-install-secrets");164		cmd.arg("decrypt").eqarg("--secret", data);165		cmd = cmd.sudo().ssh(host);166		let encoded = cmd167			.run_string()168			.await169			.context("failed to call remote host for decrypt")?170			.trim()171			.to_owned();172		z85::decode(encoded).context("bad encoded data? outdated host?")173	}174	pub async fn reencrypt_on_host(175		&self,176		host: &str,177		data: Vec<u8>,178		targets: Vec<String>,179	) -> 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 = recmd188			.run_string()189			.await190			.context("failed to call remote host for decrypt")?191			.trim()192			.to_owned();193		z85::decode(encoded).context("bad encoded data? outdated host?")194	}195196	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	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {207		let data = self.data();208		let Some(secret) = data.shared_secrets.get(secret) else {209			bail!("no shared secret {secret}");210		};211		Ok(secret.clone())212	}213	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {214		self.config_field215			.select(nix_path!(.sharedSecrets.{secret}.expectedOwners))216			.await?217			.as_json()218			.await219	}220221	pub fn save(&self) -> Result<()> {222		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;223		let data = nixlike::serialize(&self.data() as &FleetData)?;224		tempfile.write_all(225			format!(226				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",227				data228			)229			.as_bytes(),230		)?;231		let mut fleet_data_path = self.directory.clone();232		fleet_data_path.push("fleet.nix");233		tempfile.persist(fleet_data_path)?;234		Ok(())235	}236}237238#[derive(Parser, Clone)]239#[clap(group = ArgGroup::new("target_hosts"))]240pub struct FleetOpts {241	/// All hosts except those would be skipped242	#[clap(long, number_of_values = 1, group = "target_hosts")]243	only: Vec<String>,244245	/// Hosts to skip246	#[clap(long, number_of_values = 1, group = "target_hosts")]247	skip: Vec<String>,248249	/// Host, which should be threaten as current machine250	#[clap(long)]251	pub localhost: Option<String>,252253	// TODO: unhardcode x86_64-linux254	/// Override detected system for host, to perform builds via255	/// binfmt-declared qemu instead of trying to crosscompile256	#[clap(long, default_value = "detect")]257	pub local_system: String,258}259260impl FleetOpts {261	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {262		if self.localhost.is_none() {263			self.localhost264				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());265		}266		let directory = current_dir()?;267268		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;269		let root_field = pool.get().await?;270271		if self.local_system == "detect" {272			let builtins_field = Field::field(root_field.clone(), "builtins").await?;273			let system = builtins_field274				.select(nix_path!(.currentSystem))275				.await?;276			self.local_system = system.as_json().await?;277		}278		let local_system = self.local_system.clone();279280		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;281282		let fleet_field = fleet_root283			.select(nix_path!(.default.{&local_system}))284			.await?;285		let config_field = fleet_field286			.select(nix_path!(.configUnchecked))287			.await?;288289		let mut fleet_data_path = directory.clone();290		fleet_data_path.push("fleet.nix");291		let bytes = std::fs::read_to_string(fleet_data_path)?;292		let data = nixlike::parse_str(&bytes)?;293294		Ok(Config(Arc::new(FleetConfigInternals {295			opts: self,296			directory,297			data,298			local_system,299			nix_args,300			fleet_field,301			config_field,302		})))303	}304}