git.delta.rocks / jrsonnet / refs/commits / 4340a04aa508

difftreelog

source

cmds/fleet/src/host.rs7.7 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::{bail, Context, Result};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(65		&self,66		host: &str,67		mut command: MyCommand,68		sudo: bool,69	) -> Result<String> {70		if sudo {71			command = command.sudo();72		}73		if !self.is_local(host) {74			command = command.ssh(host);75		}76		command.run_string().await77	}7879	pub fn configuration_attr_name(&self, name: &str) -> OsString {80		let mut str = self.directory.as_os_str().to_owned();81		str.push("#");82		str.push(&format!(83			"fleetConfigurations.default.{}.{}",84			self.local_system, name85		));86		str87	}8889	pub async fn list_hosts(&self) -> Result<Vec<String>> {90		let mut cmd = MyCommand::new("nix");91		cmd.arg("eval")92			.arg(self.configuration_attr_name("configuredHosts"))93			.args(["--apply", "builtins.attrNames", "--json", "--show-trace"])94			.args(&self.nix_args);95		cmd.run_nix_json().await96	}97	pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {98		let mut cmd = MyCommand::new("nix");99		cmd.arg("eval")100			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))101			.args(["--json", "--show-trace"])102			.args(&self.nix_args);103		cmd.run_nix_json().await104	}105	pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {106		let mut cmd = MyCommand::new("nix");107		cmd.arg("eval")108			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))109			.args(["--apply", "builtins.attrNames"])110			.args(["--json", "--show-trace"])111			.args(&self.nix_args);112		cmd.run_nix_json().await113	}114	pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {115		let mut cmd = MyCommand::new("nix");116		cmd.arg("eval")117			.arg(118				self.configuration_attr_name(&format!(119					"configuredSystems.{}.config.{}",120					host, attr121				)),122			)123			.args(["--json", "--show-trace"])124			.args(&self.nix_args);125		cmd.run_nix_json().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 mut cmd = MyCommand::new("fleet-install-secrets");175		cmd.arg("decrypt").eqarg("--secret", data);176		cmd = cmd.sudo().ssh(host);177		let encoded = cmd178			.run_string()179			.await180			.context("failed to call remote host for decrypt")?181			.trim()182			.to_owned();183		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)184	}185	pub async fn reencrypt_on_host(186		&self,187		host: &str,188		data: Vec<u8>,189		targets: Vec<String>,190	) -> Result<Vec<u8>> {191		let data = z85::encode(&data);192		let mut recmd = MyCommand::new("fleet-install-secrets");193		recmd.arg("reencrypt").eqarg("--secret", data);194		for target in targets {195			recmd.eqarg("--targets", target);196		}197		recmd = recmd.sudo().ssh(host);198		let encoded = recmd199			.run_string()200			.await201			.context("failed to call remote host for decrypt")?202			.trim()203			.to_owned();204		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)205	}206207	#[must_use]208	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {209		let data = self.data();210		let Some(host_secrets) = data.host_secrets.get(host) else {211			bail!("no secrets for machine {host}");212		};213		let Some(secret) = host_secrets.get(secret) else {214			bail!("machine {host} has no secret {secret}");215		};216		Ok(secret.clone())217	}218	#[must_use]219	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {220		let data = self.data();221		let Some(secret) = data.shared_secrets.get(secret) else {222			bail!("no shared secret {secret}");223		};224		Ok(secret.clone())225	}226227	pub fn save(&self) -> Result<()> {228		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;229		let data = nixlike::serialize(&self.data() as &FleetData)?;230		tempfile.write_all(231			format!(232				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",233				data234			)235			.as_bytes(),236		)?;237		let mut fleet_data_path = self.directory.clone();238		fleet_data_path.push("fleet.nix");239		tempfile.persist(fleet_data_path)?;240		Ok(())241	}242}243244#[derive(Parser, Clone)]245#[clap(group = ArgGroup::new("target_hosts"))]246pub struct FleetOpts {247	/// All hosts except those would be skipped248	#[clap(long, number_of_values = 1, group = "target_hosts")]249	only: Vec<String>,250251	/// Hosts to skip252	#[clap(long, number_of_values = 1, group = "target_hosts")]253	skip: Vec<String>,254255	/// Host, which should be threaten as current machine256	#[clap(long)]257	pub localhost: Option<String>,258259	// TODO: unhardcode x86_64-linux260	/// Override detected system for host, to perform builds via261	/// binfmt-declared qemu instead of trying to crosscompile262	#[clap(long, default_value = "x86_64-linux")]263	pub local_system: String,264}265266impl FleetOpts {267	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {268		let local_system = self.local_system.clone();269		if self.localhost.is_none() {270			self.localhost271				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());272		}273		let directory = current_dir()?;274275		let mut fleet_data_path = directory.clone();276		fleet_data_path.push("fleet.nix");277		let bytes = std::fs::read_to_string(fleet_data_path)?;278		let data = nixlike::parse_str(&bytes)?;279280		Ok(Config(Arc::new(FleetConfigInternals {281			opts: self,282			directory,283			data,284			local_system,285			nix_args,286		})))287	}288}