git.delta.rocks / jrsonnet / refs/commits / 837e795f702e

difftreelog

source

cmds/fleet/src/host.rs7.5 KiBsourcehistory
1use std::{2	cell::{Ref, RefCell, RefMut},3	env::current_dir,4	ffi::{OsStr, 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;15use tokio::process::Command;1617use crate::{18	command::CommandExt,19	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},20};2122pub struct FleetConfigInternals {23	pub local_system: String,24	pub directory: PathBuf,25	pub opts: FleetOpts,26	pub data: RefCell<FleetData>,27	pub nix_args: Vec<OsString>,28}2930#[derive(Clone)]31pub struct Config(Arc<FleetConfigInternals>);3233impl Deref for Config {34	type Target = FleetConfigInternals;3536	fn deref(&self) -> &Self::Target {37		&self.038	}39}4041impl Config {42	pub fn should_skip(&self, host: &str) -> bool {43		if !self.opts.skip.is_empty() {44			self.opts.skip.iter().any(|h| h as &str == host)45		} else if !self.opts.only.is_empty() {46			!self.opts.only.iter().any(|h| h as &str == host)47		} else {48			false49		}50	}51	pub fn is_local(&self, host: &str) -> bool {52		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)53	}5455	pub fn command_on(&self, host: &str, program: impl AsRef<OsStr>, sudo: bool) -> Command {56		if self.is_local(host) {57			if sudo {58				let mut cmd = Command::new("sudo");59				cmd.arg(program);60				cmd61			} else {62				Command::new(program)63			}64		} else {65			let mut cmd = Command::new("ssh");66			cmd.arg(host).arg("--");67			if sudo {68				cmd.arg("sudo");69			}70			cmd.arg(program);71			cmd72		}73	}7475	pub fn configuration_attr_name(&self, name: &str) -> OsString {76		let mut str = self.directory.as_os_str().to_owned();77		str.push("#");78		str.push(&format!(79			"fleetConfigurations.default.{}.{}",80			self.local_system, name81		));82		str83	}8485	pub async fn list_hosts(&self) -> Result<Vec<String>> {86		Command::new("nix")87			.arg("eval")88			.arg(self.configuration_attr_name("configuredHosts"))89			.args(["--apply", "builtins.attrNames", "--json", "--show-trace"])90			.args(&self.nix_args)91			.run_nix_json()92			.await93	}94	pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {95		Command::new("nix")96			.arg("eval")97			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))98			.args(["--json", "--show-trace"])99			.args(&self.nix_args)100			.run_nix_json()101			.await102	}103	pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {104		Command::new("nix")105			.arg("eval")106			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))107			.args(["--apply", "builtins.attrNames"])108			.args(["--json", "--show-trace"])109			.args(&self.nix_args)110			.run_nix_json()111			.await112	}113	pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {114		Command::new("nix")115			.arg("eval")116			.arg(117				self.configuration_attr_name(&format!(118					"configuredSystems.{}.config.{}",119					host, attr120				)),121			)122			.args(["--json", "--show-trace"])123			.args(&self.nix_args)124			.run_nix_json()125			.await126	}127128	pub(super) fn data(&self) -> Ref<FleetData> {129		self.data.borrow()130	}131	pub(super) fn data_mut(&self) -> RefMut<FleetData> {132		self.data.borrow_mut()133	}134135	pub fn list_shared(&self) -> Vec<String> {136		let data = self.data();137		data.shared_secrets.keys().cloned().collect()138	}139	pub fn has_shared(&self, name: &str) -> bool {140		let data = self.data();141		data.shared_secrets.contains_key(name)142	}143	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {144		let mut data = self.data_mut();145		data.shared_secrets.insert(name.to_owned(), shared);146	}147	pub fn remove_shared(&self, secret: &str) {148		let mut data = self.data_mut();149		data.shared_secrets.remove(secret);150	}151152	pub fn list_secrets(&self, host: &str) -> Vec<String> {153		let data = self.data();154		let Some(host_secrets) = data.host_secrets.get(host) else {155			return Vec::new(); 156		};157		host_secrets.keys().cloned().collect()158	}159	pub fn has_secret(&self, host: &str, secret: &str) -> bool {160		let data = self.data();161		let Some(host_secrets) = data.host_secrets.get(host) else {162			return false; 163		};164		host_secrets.contains_key(secret)165	}166	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {167		let mut data = self.data_mut();168		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();169		host_secrets.insert(secret, value);170	}171172	pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>>{173		let data = z85::encode(&data);174		let encoded = self.command_on(host, "fleet-install-secrets", true)175			.arg("decrypt")176			.arg("--secret")177			.arg(data).run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();178		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)179	}180	pub async fn reencrypt_on_host(&self, host: &str, data: Vec<u8>, targets: Vec<String>) -> Result<Vec<u8>>{181		let data = z85::encode(&data);182		let mut recmd = self.command_on(host, "fleet-install-secrets", true);183		recmd184			.arg("reencrypt")185			.arg("--secret")186			.arg(format!("\"{}\"", data.replace('$', "\\$")));187		for target in targets {188			recmd.arg("--targets");189			recmd.arg(format!("\"{target}\""));190		}191		let encoded = recmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();192		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)193	}194195	#[must_use]196	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	#[must_use]207	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {208		let data = self.data();209		let Some(secret) = data.shared_secrets.get(secret) else {210			bail!("no shared secret {secret}");211		};212		Ok(secret.clone())213	}214215	pub fn save(&self) -> Result<()> {216		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;217		let data = nixlike::serialize(&self.data() as &FleetData)?;218		tempfile.write_all(219			format!(220				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",221				data222			)223			.as_bytes(),224		)?;225		let mut fleet_data_path = self.directory.clone();226		fleet_data_path.push("fleet.nix");227		tempfile.persist(fleet_data_path)?;228		Ok(())229	}230}231232#[derive(Parser, Clone)]233#[clap(group = ArgGroup::new("target_hosts"))]234pub struct FleetOpts {235	/// All hosts except those would be skipped236	#[clap(long, number_of_values = 1, group = "target_hosts")]237	only: Vec<String>,238239	/// Hosts to skip240	#[clap(long, number_of_values = 1, group = "target_hosts")]241	skip: Vec<String>,242243	/// Host, which should be threaten as current machine244	#[clap(long)]245	pub localhost: Option<String>,246247	// TODO: unhardcode x86_64-linux248	/// Override detected system for host, to perform builds via249	/// binfmt-declared qemu instead of trying to crosscompile250	#[clap(long, default_value = "x86_64-linux")]251	pub local_system: String,252}253254impl FleetOpts {255	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {256		let local_system = self.local_system.clone();257		if self.localhost.is_none() {258			self.localhost259				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());260		}261		let directory = current_dir()?;262263		let mut fleet_data_path = directory.clone();264		fleet_data_path.push("fleet.nix");265		let bytes = std::fs::read_to_string(fleet_data_path)?;266		let data = nixlike::parse_str(&bytes)?;267268		Ok(Config(Arc::new(FleetConfigInternals {269			opts: self,270			directory,271			data,272			local_system,273			nix_args,274		})))275	}276}