git.delta.rocks / jrsonnet / refs/commits / 3f73827e390b

difftreelog

source

cmds/fleet/src/host.rs7.8 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::{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	// fleetConfigurations.<name>27	pub fleet_field: Field,28	// fleet_config.configUnchecked29	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	/// Shared secrets configured in fleet.nix or in flake120	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	/// Shared secrets configured in fleet.nix128	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	/// All hosts except those would be skipped239	#[clap(long, number_of_values = 1, group = "target_hosts")]240	only: Vec<String>,241242	/// Hosts to skip243	#[clap(long, number_of_values = 1, group = "target_hosts")]244	skip: Vec<String>,245246	/// Host, which should be threaten as current machine247	#[clap(long)]248	pub localhost: Option<String>,249250	// TODO: unhardcode x86_64-linux251	/// Override detected system for host, to perform builds via252	/// binfmt-declared qemu instead of trying to crosscompile253	#[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}