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

difftreelog

feat fleet secret read command reimplementation

uzwkxlsqYaroslav Bolyukin2026-04-18parent: #0417c49.patch.diff
in: trunk

2 files changed

modifiedcmds/fleet/src/cmds/secrets.rsdiffbeforeafterboth
before · cmds/fleet/src/cmds/secrets.rs
1use std::{2	collections::{BTreeSet, HashSet},3	io::{Read, stdin},4	path::PathBuf,5};67use anyhow::{Context as _, Result, anyhow, bail, ensure};8use clap::Parser;9use fleet_base::{fleetdata::SecretOwner, host::Config, opts::FleetOpts};10use fleet_shared::SecretData;11use itertools::{ExactlyOneError, Itertools as _};12use tokio::fs::read;13use tracing::{info, warn};1415#[derive(Parser)]16pub enum Secret {17	/// Force load host keys for all defined hosts18	ForceKeys,19	/// Read secret from remote host, requires sudo on one of the owning hosts20	Read {21		/// Secret name to read22		name: String,2324		/// Distribution with what machine to read25		/// If not shared between multiple - defaults to single owner26		#[clap(short = 'm', long)]27		machine: Option<String>,2829		/// Which private secret part to read30		#[clap(short = 'p', long, default_value = "secret")]31		part: Option<String>,3233		/// Which host should we use to decrypt, in case if reencryption is required, without34		/// regeneration35		#[clap(long)]36		prefer_identities: Vec<String>,37	},38	/// Prune (remove, mark for regeneration) secrets39	Prune {40		/// Secret to prune41		name: String,4243		/// Machines to prune - if specified, only the choosen machines will be pruned44		#[clap(short = 'm', long)]45		machine: Vec<String>,46	},47	/// Ensure secret is generated and not expired48	Ensure {49		/// Secret to ensure generated50		name: String,5152		/// Machines to force secret for53		#[clap(short = 'm', long)]54		machine: Vec<String>,55	},56	List {},57	Edit {58		name: String,59		#[clap(short = 'm', long)]60		machine: String,6162		#[clap(long)]63		add: bool,6465		/// Which private secret part to read66		#[clap(short = 'p', long, default_value = "secret")]67		part: String,68	},69}7071impl Secret {72	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {73		match self {74			Secret::ForceKeys => {75				for host in config.list_hosts()? {76					if opts.should_skip(&host)? {77						continue;78					}79					config.host_key(&host.name).await?;80				}81			}82			Secret::Read {83				name,84				machine,85				part: part_name,86				mut prefer_identities,87			} => {88				let secret = config.data.secrets.read().expect("not poisoned");8990				let Some(dist) = secret.get("name") else {91					bail!("secret doesn't exists");92				};9394				let dist = if let Some(machine) = &machine {95					dist.get(&SecretOwner::host(machine))96						.ok_or_else(|| anyhow!("machine {machine} has no secret generated"))?97				} else {98					dist.distributions()99						.exactly_one()100						.map_err(|e| anyhow!("{e}"))101						.context(102							"with no machine specified, there should be exactly one distribution",103						)?104				};105106				let part_name = part_name.unwrap_or_else(|| "secret".to_string());107				let Some(part) = dist.secret.parts.get(&part_name) else {108					bail!("secret part {part_name:?} is not defined");109				};110111				// dist.get(SecretOwner(name));112113				todo!();114				/*115				let Some(secret) = config.shared_secret(&name) else {116					bail!("secret doesn't exists");117				};118119				let dist = if secret.len() == 1 {120					&secret[0]121				} else if let Some(machine) = machine {122					let dist = secret.get(&machine);123					let Some(dist) = dist else {124						bail!("machine {machine} has no distribution of secret {name}");125					};126					prefer_identities.push(machine);127					dist128				} else {129					bail!(130						"secret {name} has shares, but no --machine specified for specifing which do you need"131					)132				};133134				let Some(part) = dist.secret.parts.get(&part_name) else {135					bail!("no part {part_name} in secret {name}");136				};137				let data = if part.raw.encrypted {138					let identity_holder = if !prefer_identities.is_empty() {139						prefer_identities140							.iter()141							.find(|i| dist.owners.iter().any(|s| s == *i))142					} else {143						dist.owners.first()144					};145					let Some(identity_holder) = identity_holder else {146						bail!("no available holder found");147					};148					let host = config.host(identity_holder)?;149					host.decrypt(part.raw.clone()).await?150				} else {151					part.raw.data.clone()152				};153				stdout().write_all(&data)?;154				*/155				todo!()156			}157			Secret::List {} => {158				/*159				let _span = info_span!("loading secrets").entered();160				let configured = config.list_configured_shared()?;161				#[derive(Tabled)]162				struct SecretDisplay {163					#[tabled(rename = "Name")]164					name: String,165					#[tabled(rename = "Owners")]166					owners: String,167				}168				// let mut table = vec![];169				for name in configured.iter().cloned() {170					let config = config.clone();171					let data = config.shared_secret(&name).expect("exists");172					/*173										let definition = config.shared_secret_definition(&name)?;174										let expectations = definition.expectations()?;175										let owners = data176											.owners()177											.map(|o| {178												if expectations.owners.contains(o) {179													o.green().to_string()180												} else {181													o.red().to_string()182												}183											})184											.collect::<Vec<_>>();185										table.push(SecretDisplay {186											owners: owners.join(", "),187											name,188										})189					*/190				}191				// info!("loaded\n{}", Table::new(table).to_string())192				*/193				todo!()194			}195			Secret::Edit {196				name,197				machine,198				part,199				add,200			} => {201				/*let secret = config202					.host_secret(&machine, &name)203					.context("secret not found")?;204				if let Some(data) = secret.secret.parts.get(&part) {205					let host = config.host(&machine)?;206					let secret = host.decrypt(data.raw.clone()).await?;207					String::from_utf8(secret).context("secret is not utf8")?208				} else if add {209					String::new()210				} else {211					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");212				};*/213				todo!()214			}215			Secret::Prune { name, machine } => todo!(),216			Secret::Ensure { name, machine } => todo!(),217		}218		Ok(())219	}220}221222/*223async fn edit_temp_file(224	builder: tempfile::Builder<'_, '_>,225	r: Vec<u8>,226	header: &str,227	comment: &str,228) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {229	if !stdin().is_tty() {230		// TODO: Also try to open /dev/tty directly?231		bail!("stdin is not tty, can't open editor");232	}233234	use std::fmt::Write;235	let mut file = builder.tempfile()?;236237	let mut full_header = String::new();238	let mut had = false;239	for line in header.trim_end().lines() {240		had = true;241		writeln!(&mut full_header, "{comment}{line}")?;242	}243	if had {244		writeln!(&mut full_header, "{}", comment.trim_end())?;245	}246	writeln!(247		&mut full_header,248		"{comment}Do not touch this header! It will be removed automatically"249	)?;250251	file.write_all(full_header.as_bytes())?;252	file.write_all(&r)?;253254	let abs_path = file.into_temp_path();255	let editor = std::env::var_os("VISUAL")256		.or_else(|| std::env::var_os("EDITOR"))257		.unwrap_or_else(|| "vi".into());258	let editor_args = shlex::bytes::split(editor.as_encoded_bytes())259		.ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;260	let editor_args = editor_args261		.into_iter()262		.map(|v| {263			// Only ASCII subsequences are replaced264			unsafe { OsString::from_encoded_bytes_unchecked(v) }265		})266		.collect_vec();267	let Some((editor, args)) = editor_args.split_first() else {268		bail!("EDITOR env var has no command");269	};270	let mut command = Command::new(editor);271	command.args(args);272273	let path_arg = abs_path.canonicalize()?;274275	// TODO: Save full state, using tcget/_getmode/_setmode276	let was_raw = terminal::is_raw_mode_enabled()?;277	terminal::enable_raw_mode()?;278279	let status = command.arg(path_arg).status().await;280281	if !was_raw {282		terminal::disable_raw_mode()?;283	}284285	let success = match status {286		Ok(s) => s.success(),287		Err(e) if e.kind() == io::ErrorKind::NotFound => {288			bail!("editor not found")289		}290		Err(e) => bail!("editor spawn error: {e}"),291	};292293	let mut file = std::fs::read(&abs_path).context("read editor output")?;294	let Some(v) = file.strip_prefix(full_header.as_bytes()) else {295		todo!();296	};297	todo!();298299	// Ok((success, abs_path))300}301*/
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -1,5 +1,5 @@
 use std::{
-	collections::{BTreeMap, BTreeSet},
+	collections::{BTreeMap, BTreeSet, HashSet},
 	ffi::{OsStr, OsString},
 	fmt::Display,
 	io::Write,
@@ -652,6 +652,23 @@
 		}
 	}
 
+	pub fn preferred_hosts(
+		&self,
+		filter: impl Fn(&str) -> bool,
+	) -> Result<impl Iterator<Item = Result<ConfigHost>>> {
+		let prefer = self
+			.prefer_identities
+			.iter()
+			.filter_map(|v| v.as_host())
+			.collect::<HashSet<_>>();
+		let config = &self.config_field;
+		let mut names = nix_go!(config.hosts).list_fields()?;
+		names.retain(|s| filter(s));
+		names.sort_by_key(|h| prefer.contains(h.as_str()));
+
+		Ok(names.into_iter().map(|h| self.host(&h)))
+	}
+
 	pub fn host(&self, name: &str) -> Result<ConfigHost> {
 		let config = &self.config_field;
 		let host_config = nix_go!(config.hosts[{ name }]);