git.delta.rocks / jrsonnet / refs/commits / e85b4da8a439

difftreelog

source

cmds/fleet/src/host.rs8.1 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};2021pub struct FleetConfigInternals {22	pub local_system: String,23	pub directory: PathBuf,24	pub opts: FleetOpts,25	pub data: Mutex<FleetData>,26	pub nix_args: Vec<OsString>,27	// fleetConfigurations.<name>28	pub fleet_field: Field,29	// fleet_config.configUnchecked30	pub config_field: Field,31}3233#[derive(Clone)]34pub struct Config(Arc<FleetConfigInternals>);3536impl Deref for Config {37	type Target = FleetConfigInternals;3839	fn deref(&self) -> &Self::Target {40		&self.041	}42}4344pub struct ConfigHost {45	pub name: String,46}47impl ConfigHost {48	async fn open_session(&self) -> Result<openssh::Session> {49		let mut session = SessionBuilder::default();5051		session52			.connect(&self.name)53			.await54			.map_err(|e| anyhow!("ssh error: {e}"))55	}56}5758impl Config {59	pub fn should_skip(&self, host: &str) -> bool {60		if !self.opts.skip.is_empty() {61			self.opts.skip.iter().any(|h| h as &str == host)62		} else if !self.opts.only.is_empty() {63			!self.opts.only.iter().any(|h| h as &str == host)64		} else {65			false66		}67	}68	pub fn is_local(&self, host: &str) -> bool {69		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)70	}7172	pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {73		if sudo {74			command = command.sudo();75		}76		if !self.is_local(host) {77			command = command.ssh(host);78		}79		command.run().await80	}81	pub async fn run_string_on(82		&self,83		host: &str,84		mut command: MyCommand,85		sudo: bool,86	) -> Result<String> {87		if sudo {88			command = command.sudo();89		}90		if !self.is_local(host) {91			command = command.ssh(host);92		}93		command.run_string().await94	}9596	pub fn configuration_attr_name(&self, name: &str) -> OsString {97		let mut str = self.directory.as_os_str().to_owned();98		str.push("#");99		str.push(&format!(100			"fleetConfigurations.default.{}.{}",101			self.local_system, name102		));103		str104	}105106	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {107		let names = self108			.fleet_field109			.get_field_deep(["configuredHosts"])110			.await?111			.list_fields()112			.await?;113		let mut out = vec![];114		for name in names {115			out.push(ConfigHost { name })116		}117		Ok(out)118	}119	pub async fn system_config(&self, host: &str) -> Result<Field> {120		self.fleet_field121			.get_field_deep(["configuredSystems", host, "config"])122			.await123	}124125	pub(super) fn data(&self) -> MutexGuard<FleetData> {126		self.data.lock().unwrap()127	}128	pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {129		self.data.lock().unwrap()130	}131	/// Shared secrets configured in fleet.nix or in flake132	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {133		self.config_field134			.get_field("sharedSecrets")135			.await?136			.list_fields()137			.await138	}139	/// Shared secrets configured in fleet.nix140	pub fn list_shared(&self) -> Vec<String> {141		let data = self.data();142		data.shared_secrets.keys().cloned().collect()143	}144	pub fn has_shared(&self, name: &str) -> bool {145		let data = self.data();146		data.shared_secrets.contains_key(name)147	}148	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {149		let mut data = self.data_mut();150		data.shared_secrets.insert(name.to_owned(), shared);151	}152	pub fn remove_shared(&self, secret: &str) {153		let mut data = self.data_mut();154		data.shared_secrets.remove(secret);155	}156157	pub fn has_secret(&self, host: &str, secret: &str) -> bool {158		let data = self.data();159		let Some(host_secrets) = data.host_secrets.get(host) else {160			return false;161		};162		host_secrets.contains_key(secret)163	}164	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {165		let mut data = self.data_mut();166		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();167		host_secrets.insert(secret, value);168	}169170	pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>> {171		let data = z85::encode(&data);172		let mut cmd = MyCommand::new("fleet-install-secrets");173		cmd.arg("decrypt").eqarg("--secret", data);174		cmd = cmd.sudo().ssh(host);175		let encoded = cmd176			.run_string()177			.await178			.context("failed to call remote host for decrypt")?179			.trim()180			.to_owned();181		z85::decode(encoded).context("bad encoded data? outdated host?")182	}183	pub async fn reencrypt_on_host(184		&self,185		host: &str,186		data: Vec<u8>,187		targets: Vec<String>,188	) -> Result<Vec<u8>> {189		let data = z85::encode(&data);190		let mut recmd = MyCommand::new("fleet-install-secrets");191		recmd.arg("reencrypt").eqarg("--secret", data);192		for target in targets {193			recmd.eqarg("--targets", target);194		}195		recmd = recmd.sudo().ssh(host);196		let encoded = recmd197			.run_string()198			.await199			.context("failed to call remote host for decrypt")?200			.trim()201			.to_owned();202		z85::decode(encoded).context("bad encoded data? outdated host?")203	}204205	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {206		let data = self.data();207		let Some(host_secrets) = data.host_secrets.get(host) else {208			bail!("no secrets for machine {host}");209		};210		let Some(secret) = host_secrets.get(secret) else {211			bail!("machine {host} has no secret {secret}");212		};213		Ok(secret.clone())214	}215	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {216		let data = self.data();217		let Some(secret) = data.shared_secrets.get(secret) else {218			bail!("no shared secret {secret}");219		};220		Ok(secret.clone())221	}222	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {223		self.config_field224			.get_field_deep(["sharedSecrets", secret, "expectedOwners"])225			.await?226			.as_json()227			.await228	}229230	pub fn save(&self) -> Result<()> {231		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;232		let data = nixlike::serialize(&self.data() as &FleetData)?;233		tempfile.write_all(234			format!(235				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",236				data237			)238			.as_bytes(),239		)?;240		let mut fleet_data_path = self.directory.clone();241		fleet_data_path.push("fleet.nix");242		tempfile.persist(fleet_data_path)?;243		Ok(())244	}245}246247#[derive(Parser, Clone)]248#[clap(group = ArgGroup::new("target_hosts"))]249pub struct FleetOpts {250	/// All hosts except those would be skipped251	#[clap(long, number_of_values = 1, group = "target_hosts")]252	only: Vec<String>,253254	/// Hosts to skip255	#[clap(long, number_of_values = 1, group = "target_hosts")]256	skip: Vec<String>,257258	/// Host, which should be threaten as current machine259	#[clap(long)]260	pub localhost: Option<String>,261262	// TODO: unhardcode x86_64-linux263	/// Override detected system for host, to perform builds via264	/// binfmt-declared qemu instead of trying to crosscompile265	#[clap(long, default_value = "detect")]266	pub local_system: String,267}268269impl FleetOpts {270	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {271		if self.localhost.is_none() {272			self.localhost273				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());274		}275		let directory = current_dir()?;276277		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;278		let root_field = pool.get().await?;279280		if self.local_system == "detect" {281			let builtins_field = Field::field(root_field.clone(), "builtins").await?;282			let system = builtins_field.get_field("currentSystem").await?;283			self.local_system = system.as_json().await?;284		}285		let local_system = self.local_system.clone();286287		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;288289		let fleet_field = fleet_root290			.get_field_deep(["default", &local_system])291			.await?;292		let config_field = fleet_field.get_field("configUnchecked").await?;293294		let mut fleet_data_path = directory.clone();295		fleet_data_path.push("fleet.nix");296		let bytes = std::fs::read_to_string(fleet_data_path)?;297		let data = nixlike::parse_str(&bytes)?;298299		Ok(Config(Arc::new(FleetConfigInternals {300			opts: self,301			directory,302			data,303			local_system,304			nix_args,305			fleet_field,306			config_field,307		})))308	}309}