git.delta.rocks / jrsonnet / refs/commits / 7b7c4bb80b01

difftreelog

feat fleet secret regenerate subcommand

puknpnklYaroslav Bolyukin2026-04-18parent: #204755f.patch.diff
in: trunk

1 file changed

modifiedcmds/fleet/src/cmds/secrets.rsdiffbeforeafterboth
before · cmds/fleet/src/cmds/secrets.rs
1use std::collections::BTreeSet;2use std::io::{Write as _, stdout};34use anyhow::{Context as _, Result, anyhow, bail};5use clap::Parser;6use fleet_base::{fleetdata::SecretOwner, host::Config, opts::FleetOpts};7use itertools::Itertools as _;8use nix_eval::nix_go;9use tracing::{info, warn};1011#[derive(Parser)]12pub enum Secret {13	/// Force load host keys for all defined hosts14	ForceKeys,15	/// Read secret from remote host, requires sudo on one of the owning hosts16	Read {17		/// Secret name to read18		name: String,1920		/// Distribution with what machine to read21		/// If not shared between multiple - defaults to single owner22		#[clap(short = 'm', long)]23		machine: Option<String>,2425		/// Which private secret part to read26		/// If not specified - only one existing part is read27		#[clap(short = 'p', long)]28		part: Option<String>,29	},30	/// Prune (remove, mark for regeneration) secrets31	Prune {32		/// Secret to prune33		name: String,3435		/// Machines to prune - if specified, only the choosen machines will be pruned36		#[clap(short = 'm', long)]37		machine: Vec<String>,3839		/// If set - distributions containing the specified machines will be pruned fully40		#[clap(long)]41		whole_dist: bool,42	},43	/// Ensure secret is generated and not expired44	Ensure {45		/// Secret to ensure generated46		name: String,4748		/// Machines to force secret for49		#[clap(short = 'm', long)]50		machine: Vec<String>,51	},52	List {},53}5455impl Secret {56	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {57		match self {58			Secret::ForceKeys => {59				for host in config.list_hosts()? {60					if opts.should_skip(&host)? {61						continue;62					}63					config.host_key(&host.name).await?;64				}65			}66			Secret::Read {67				name,68				machine,69				part: part_name,70			} => {71				let (owners, secret_data) = {72					let secret = config.data.secrets.read().expect("not poisoned");7374					let Some(dist) = secret.get(&name) else {75						bail!("secret doesn't exists");76					};7778					let dist = if let Some(machine) = &machine {79						dist.get(&SecretOwner::host(machine))80							.ok_or_else(|| anyhow!("machine {machine} has no secret generated"))?81					} else {82						dist.distributions()83							.exactly_one()84							.map_err(|e| anyhow!("{e}"))85							.context(86								"with no machine specified, there should be exactly one distribution",87							)?88					};8990					let part = if let Some(part_name) = &part_name {91						dist.secret.parts.get(part_name).ok_or_else(|| {92							anyhow!("secret {name} does not have part named {part_name}")93						})?94					} else {95						dist.secret96							.parts97							.iter()98							.exactly_one()99							.map_err(|e| anyhow!("{e}"))100							.context("with no part specified, there should be exactly one part")?101							.1102					};103					let owners = dist.owners().cloned().collect::<Vec<_>>();104					let secret_data = part.raw.clone();105					(owners, secret_data)106				};107108				for host in config109					.preferred_hosts(|h| owners.iter().any(|o| o.as_host() == Some(h)))110					.context("failed to list hosts")?111				{112					let host = match host {113						Ok(h) => h,114						Err(e) => {115							warn!("failed to use host: {e}");116							continue;117						}118					};119					match host.decrypt(secret_data.clone()).await {120						Ok(data) => {121							let mut w = stdout();122							w.write_all(&data)?;123							return Ok(());124						}125						Err(e) => warn!("failed to decrypt on {}: {e}", host.name),126					};127				}128				bail!("failed to find suitable decrypting host");129			}130			Secret::List {} => {131				/*132				let _span = info_span!("loading secrets").entered();133				let configured = config.list_configured_shared()?;134				#[derive(Tabled)]135				struct SecretDisplay {136					#[tabled(rename = "Name")]137					name: String,138					#[tabled(rename = "Owners")]139					owners: String,140				}141				// let mut table = vec![];142				for name in configured.iter().cloned() {143					let config = config.clone();144					let data = config.shared_secret(&name).expect("exists");145					/*146										let definition = config.shared_secret_definition(&name)?;147										let expectations = definition.expectations()?;148										let owners = data149											.owners()150											.map(|o| {151												if expectations.owners.contains(o) {152													o.green().to_string()153												} else {154													o.red().to_string()155												}156											})157											.collect::<Vec<_>>();158										table.push(SecretDisplay {159											owners: owners.join(", "),160											name,161										})162					*/163				}164				// info!("loaded\n{}", Table::new(table).to_string())165				*/166				todo!()167			}168			Secret::Prune {169				name,170				machine,171				whole_dist,172			} => {173				let mut secrets = config.data.secrets.write().expect("not poisoned");174				let Some(dists) = secrets.get_mut(&name) else {175					bail!("secret {name} not found");176				};177				if machine.is_empty() && whole_dist {178					for dist in dists.distributions_mut() {179						dist.prune("manual prune".to_owned());180					}181				} else if machine.is_empty() {182					let dist = dists183						.distributions_mut()184						.exactly_one()185						.map_err(|e| anyhow!("{e}"))186						.context(187							"with no machine specified, there should be exactly one distribution",188						)?;189					dist.prune("manual prune".to_owned());190				} else if whole_dist {191					for dist in dists.distributions_mut() {192						if machine193							.iter()194							.any(|m| dist.owners().any(|o| o.as_host() == Some(m.as_str())))195						{196							dist.prune(format!(197								"manual prune of distribution containing {}",198								machine.join(", ")199							));200						}201					}202				} else {203					let owners: BTreeSet<SecretOwner> =204						machine.iter().map(SecretOwner::host).collect();205					for dist in dists.distributions_mut() {206						dist.prune_owners(&owners, "manual prune".to_owned());207					}208				}209			}210			Secret::Ensure { name, machine } => {211				let hosts: Vec<String> = if machine.is_empty() {212					config213						.list_hosts()?214						.into_iter()215						.filter(|h| opts.should_skip(h).ok() != Some(true))216						.map(|h| h.name)217						.collect()218				} else {219					machine220				};221222				for hostname in &hosts {223					let nixos_cfg = config.system_config(hostname)?;224					let secrets = nix_go!(nixos_cfg.secrets);225					if secrets.has_field(&name)? {226						info!("ensuring secret {name} for {hostname}");227						// Force evaluation of secret parts, triggering __fleetEnsureHostSecret228						nix_go!(secrets[{ &name }].definition.parts);229					}230				}231			}232		}233		Ok(())234	}235}