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

difftreelog

source

cmds/fleet/src/host.rs7.6 KiBsourcehistory
1use std::{2	cell::{Ref, RefCell, RefMut},3	env::current_dir,4	ffi::OsString,5	io::Write,6	ops::Deref,7	path::PathBuf,8	sync::Arc,9};1011use anyhow::{Result, bail, Context};12use clap::{ArgGroup, Parser};13use serde::de::DeserializeOwned;14use tempfile::NamedTempFile;1516use crate::{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: RefCell<FleetData>,26	pub nix_args: Vec<OsString>,27}2829#[derive(Clone)]30pub struct Config(Arc<FleetConfigInternals>);3132impl Deref for Config {33	type Target = FleetConfigInternals;3435	fn deref(&self) -> &Self::Target {36		&self.037	}38}3940impl Config {41	pub fn should_skip(&self, host: &str) -> bool {42		if !self.opts.skip.is_empty() {43			self.opts.skip.iter().any(|h| h as &str == host)44		} else if !self.opts.only.is_empty() {45			!self.opts.only.iter().any(|h| h as &str == host)46		} else {47			false48		}49	}50	pub fn is_local(&self, host: &str) -> bool {51		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)52	}5354	pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {55		if sudo {56			command = command.sudo();57		}58		if !self.is_local(host) {59			command = command.ssh(host);60		}61		command.run().await62	}63	#[must_use]64	pub async fn run_string_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<String> {65		if sudo {66			command = command.sudo();67		}68		if !self.is_local(host) {69			command = command.ssh(host);70		}71		command.run_string().await72	}7374	pub fn configuration_attr_name(&self, name: &str) -> OsString {75		let mut str = self.directory.as_os_str().to_owned();76		str.push("#");77		str.push(&format!(78			"fleetConfigurations.default.{}.{}",79			self.local_system, name80		));81		str82	}8384	pub async fn list_hosts(&self) -> Result<Vec<String>> {85		let mut cmd = MyCommand::new("nix");86		cmd.arg("eval")87			.arg(self.configuration_attr_name("configuredHosts"))88			.args(["--apply", "builtins.attrNames", "--json", "--show-trace"])89			.args(&self.nix_args);90		cmd.run_nix_json()91			.await92	}93	pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {94		let mut cmd = MyCommand::new("nix");95		cmd.arg("eval")96			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))97			.args(["--json", "--show-trace"])98			.args(&self.nix_args);99		cmd.run_nix_json()100			.await101	}102	pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {103		let mut cmd = MyCommand::new("nix");104		cmd.arg("eval")105			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))106			.args(["--apply", "builtins.attrNames"])107			.args(["--json", "--show-trace"])108			.args(&self.nix_args);109		cmd.run_nix_json()110			.await111	}112	pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {113		let mut cmd = MyCommand::new("nix");114		cmd.arg("eval")115			.arg(116				self.configuration_attr_name(&format!(117					"configuredSystems.{}.config.{}",118					host, attr119				)),120			)121			.args(["--json", "--show-trace"])122			.args(&self.nix_args);123		cmd.run_nix_json()124			.await125	}126127	pub(super) fn data(&self) -> Ref<FleetData> {128		self.data.borrow()129	}130	pub(super) fn data_mut(&self) -> RefMut<FleetData> {131		self.data.borrow_mut()132	}133134	pub fn list_shared(&self) -> Vec<String> {135		let data = self.data();136		data.shared_secrets.keys().cloned().collect()137	}138	pub fn has_shared(&self, name: &str) -> bool {139		let data = self.data();140		data.shared_secrets.contains_key(name)141	}142	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {143		let mut data = self.data_mut();144		data.shared_secrets.insert(name.to_owned(), shared);145	}146	pub fn remove_shared(&self, secret: &str) {147		let mut data = self.data_mut();148		data.shared_secrets.remove(secret);149	}150151	pub fn list_secrets(&self, host: &str) -> Vec<String> {152		let data = self.data();153		let Some(host_secrets) = data.host_secrets.get(host) else {154			return Vec::new(); 155		};156		host_secrets.keys().cloned().collect()157	}158	pub fn has_secret(&self, host: &str, secret: &str) -> bool {159		let data = self.data();160		let Some(host_secrets) = data.host_secrets.get(host) else {161			return false; 162		};163		host_secrets.contains_key(secret)164	}165	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {166		let mut data = self.data_mut();167		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();168		host_secrets.insert(secret, value);169	}170171	pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>>{172		let data = z85::encode(&data);173		let mut cmd = MyCommand::new("fleet-install-secrets");174		cmd.arg("decrypt").eqarg("--secret", data);175		cmd = cmd.sudo().ssh(host);176		let encoded = cmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();177		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)178	}179	pub async fn reencrypt_on_host(&self, host: &str, data: Vec<u8>, targets: Vec<String>) -> 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 = recmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();188		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)189	}190191	#[must_use]192	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {193		let data = self.data();194		let Some(host_secrets) = data.host_secrets.get(host) else {195            bail!("no secrets for machine {host}");196        };197		let Some(secret) = host_secrets.get(secret) else {198            bail!("machine {host} has no secret {secret}");199        };200		Ok(secret.clone())201	}202	#[must_use]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	}210211	pub fn save(&self) -> Result<()> {212		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;213		let data = nixlike::serialize(&self.data() as &FleetData)?;214		tempfile.write_all(215			format!(216				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",217				data218			)219			.as_bytes(),220		)?;221		let mut fleet_data_path = self.directory.clone();222		fleet_data_path.push("fleet.nix");223		tempfile.persist(fleet_data_path)?;224		Ok(())225	}226}227228#[derive(Parser, Clone)]229#[clap(group = ArgGroup::new("target_hosts"))]230pub struct FleetOpts {231	/// All hosts except those would be skipped232	#[clap(long, number_of_values = 1, group = "target_hosts")]233	only: Vec<String>,234235	/// Hosts to skip236	#[clap(long, number_of_values = 1, group = "target_hosts")]237	skip: Vec<String>,238239	/// Host, which should be threaten as current machine240	#[clap(long)]241	pub localhost: Option<String>,242243	// TODO: unhardcode x86_64-linux244	/// Override detected system for host, to perform builds via245	/// binfmt-declared qemu instead of trying to crosscompile246	#[clap(long, default_value = "x86_64-linux")]247	pub local_system: String,248}249250impl FleetOpts {251	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {252		let local_system = self.local_system.clone();253		if self.localhost.is_none() {254			self.localhost255				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());256		}257		let directory = current_dir()?;258259		let mut fleet_data_path = directory.clone();260		fleet_data_path.push("fleet.nix");261		let bytes = std::fs::read_to_string(fleet_data_path)?;262		let data = nixlike::parse_str(&bytes)?;263264		Ok(Config(Arc::new(FleetConfigInternals {265			opts: self,266			directory,267			data,268			local_system,269			nix_args,270		})))271	}272}