difftreelog
feat fleet secret ensure subcommand
in: trunk
1 file changed
cmds/fleet/src/cmds/secrets.rsdiffbeforeafterboth1use 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}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}