git.delta.rocks / jrsonnet / refs/commits / 48f2bca6ca28

difftreelog

feat fleet secret prune command implementation

owzlypyvYaroslav Bolyukin2026-04-18parent: #c4cd98e.patch.diff
in: trunk

1 file changed

modifiedcmds/fleet/src/cmds/secrets.rsdiffbeforeafterboth
after · 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 tracing::warn;910#[derive(Parser)]11pub enum Secret {12	/// Force load host keys for all defined hosts13	ForceKeys,14	/// Read secret from remote host, requires sudo on one of the owning hosts15	Read {16		/// Secret name to read17		name: String,1819		/// Distribution with what machine to read20		/// If not shared between multiple - defaults to single owner21		#[clap(short = 'm', long)]22		machine: Option<String>,2324		/// Which private secret part to read25		/// If not specified - only one existing part is read26		#[clap(short = 'p', long)]27		part: Option<String>,28	},29	/// Prune (remove, mark for regeneration) secrets30	Prune {31		/// Secret to prune32		name: String,3334		/// Machines to prune - if specified, only the choosen machines will be pruned35		#[clap(short = 'm', long)]36		machine: Vec<String>,3738		/// If set - distributions containing the specified machines will be pruned fully39		#[clap(long)]40		whole_dist: bool,41	},42	/// Ensure secret is generated and not expired43	Ensure {44		/// Secret to ensure generated45		name: String,4647		/// Machines to force secret for48		#[clap(short = 'm', long)]49		machine: Vec<String>,50	},51	List {},52}5354impl Secret {55	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {56		match self {57			Secret::ForceKeys => {58				for host in config.list_hosts()? {59					if opts.should_skip(&host)? {60						continue;61					}62					config.host_key(&host.name).await?;63				}64			}65			Secret::Read {66				name,67				machine,68				part: part_name,69			} => {70				let (owners, secret_data) = {71					let secret = config.data.secrets.read().expect("not poisoned");7273					let Some(dist) = secret.get(&name) else {74						bail!("secret doesn't exists");75					};7677					let dist = if let Some(machine) = &machine {78						dist.get(&SecretOwner::host(machine))79							.ok_or_else(|| anyhow!("machine {machine} has no secret generated"))?80					} else {81						dist.distributions()82							.exactly_one()83							.map_err(|e| anyhow!("{e}"))84							.context(85								"with no machine specified, there should be exactly one distribution",86							)?87					};8889					let part = if let Some(part_name) = &part_name {90						dist.secret.parts.get(part_name).ok_or_else(|| {91							anyhow!("secret {name} does not have part named {part_name}")92						})?93					} else {94						dist.secret95							.parts96							.iter()97							.exactly_one()98							.map_err(|e| anyhow!("{e}"))99							.context("with no part specified, there should be exactly one part")?100							.1101					};102					let owners = dist.owners().cloned().collect::<Vec<_>>();103					let secret_data = part.raw.clone();104					(owners, secret_data)105				};106107				for host in config108					.preferred_hosts(|h| owners.iter().any(|o| o.as_host() == Some(h)))109					.context("failed to list hosts")?110				{111					let host = match host {112						Ok(h) => h,113						Err(e) => {114							warn!("failed to use host: {e}");115							continue;116						}117					};118					match host.decrypt(secret_data.clone()).await {119						Ok(data) => {120							let mut w = stdout();121							w.write_all(&data)?;122							return Ok(());123						}124						Err(e) => warn!("failed to decrypt on {}: {e}", host.name),125					};126				}127				bail!("failed to find suitable decrypting host");128			}129			Secret::List {} => {130				/*131				let _span = info_span!("loading secrets").entered();132				let configured = config.list_configured_shared()?;133				#[derive(Tabled)]134				struct SecretDisplay {135					#[tabled(rename = "Name")]136					name: String,137					#[tabled(rename = "Owners")]138					owners: String,139				}140				// let mut table = vec![];141				for name in configured.iter().cloned() {142					let config = config.clone();143					let data = config.shared_secret(&name).expect("exists");144					/*145										let definition = config.shared_secret_definition(&name)?;146										let expectations = definition.expectations()?;147										let owners = data148											.owners()149											.map(|o| {150												if expectations.owners.contains(o) {151													o.green().to_string()152												} else {153													o.red().to_string()154												}155											})156											.collect::<Vec<_>>();157										table.push(SecretDisplay {158											owners: owners.join(", "),159											name,160										})161					*/162				}163				// info!("loaded\n{}", Table::new(table).to_string())164				*/165				todo!()166			}167			Secret::Prune {168				name,169				machine,170				whole_dist,171			} => {172				let mut secrets = config.data.secrets.write().expect("not poisoned");173				let Some(dists) = secrets.get_mut(&name) else {174					bail!("secret {name} not found");175				};176				if machine.is_empty() && whole_dist {177					for dist in dists.distributions_mut() {178						dist.prune("manual prune".to_owned());179					}180				} else if machine.is_empty() {181					let dist = dists182						.distributions_mut()183						.exactly_one()184						.map_err(|e| anyhow!("{e}"))185						.context(186							"with no machine specified, there should be exactly one distribution",187						)?;188					dist.prune("manual prune".to_owned());189				} else if whole_dist {190					for dist in dists.distributions_mut() {191						if machine192							.iter()193							.any(|m| dist.owners().any(|o| o.as_host() == Some(m.as_str())))194						{195							dist.prune(format!(196								"manual prune of distribution containing {}",197								machine.join(", ")198							));199						}200					}201				} else {202					let owners: BTreeSet<SecretOwner> =203						machine.iter().map(SecretOwner::host).collect();204					for dist in dists.distributions_mut() {205						dist.prune_owners(&owners, "manual prune".to_owned());206					}207				}208			}209			Secret::Ensure { name, machine } => todo!(),210		}211		Ok(())212	}213}