1use std::io::{Write as _, stdout};23use anyhow::{Context as _, Result, anyhow, bail};4use clap::Parser;5use fleet_base::{fleetdata::SecretOwner, host::Config, opts::FleetOpts};6use itertools::Itertools as _;7use tracing::warn;89#[derive(Parser)]10pub enum Secret {11 12 ForceKeys,13 14 Read {15 16 name: String,1718 19 20 #[clap(short = 'm', long)]21 machine: Option<String>,2223 24 25 #[clap(short = 'p', long)]26 part: Option<String>,27 },28 29 Prune {30 31 name: String,3233 34 #[clap(short = 'm', long)]35 machine: Vec<String>,36 },37 38 Ensure {39 40 name: String,4142 43 #[clap(short = 'm', long)]44 machine: Vec<String>,45 },46 List {},47}4849impl Secret {50 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {51 match self {52 Secret::ForceKeys => {53 for host in config.list_hosts()? {54 if opts.should_skip(&host)? {55 continue;56 }57 config.host_key(&host.name).await?;58 }59 }60 Secret::Read {61 name,62 machine,63 part: part_name,64 } => {65 let (owners, secret_data) = {66 let secret = config.data.secrets.read().expect("not poisoned");6768 let Some(dist) = secret.get(&name) else {69 bail!("secret doesn't exists");70 };7172 let dist = if let Some(machine) = &machine {73 dist.get(&SecretOwner::host(machine))74 .ok_or_else(|| anyhow!("machine {machine} has no secret generated"))?75 } else {76 dist.distributions()77 .exactly_one()78 .map_err(|e| anyhow!("{e}"))79 .context(80 "with no machine specified, there should be exactly one distribution",81 )?82 };8384 let part = if let Some(part_name) = &part_name {85 dist.secret.parts.get(part_name).ok_or_else(|| {86 anyhow!("secret {name} does not have part named {part_name}")87 })?88 } else {89 dist.secret90 .parts91 .iter()92 .exactly_one()93 .map_err(|e| anyhow!("{e}"))94 .context("with no part specified, there should be exactly one part")?95 .196 };97 let owners = dist.owners().cloned().collect::<Vec<_>>();98 let secret_data = part.raw.clone();99 (owners, secret_data)100 };101102 for host in config103 .preferred_hosts(|h| owners.iter().any(|o| o.as_host() == Some(h)))104 .context("failed to list hosts")?105 {106 let host = match host {107 Ok(h) => h,108 Err(e) => {109 warn!("failed to use host: {e}");110 continue;111 }112 };113 match host.decrypt(secret_data.clone()).await {114 Ok(data) => {115 let mut w = stdout();116 w.write_all(&data)?;117 return Ok(());118 }119 Err(e) => warn!("failed to decrypt on {}: {e}", host.name),120 };121 }122 bail!("failed to find suitable decrypting host");123 }124 Secret::List {} => {125 126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 todo!()161 }162 Secret::Prune { name, machine } => todo!(),163 Secret::Ensure { name, machine } => todo!(),164 }165 Ok(())166 }167}168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
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 13 ForceKeys,14 15 Read {16 17 name: String,1819 20 21 #[clap(short = 'm', long)]22 machine: Option<String>,2324 25 26 #[clap(short = 'p', long)]27 part: Option<String>,28 },29 30 Prune {31 32 name: String,3334 35 #[clap(short = 'm', long)]36 machine: Vec<String>,3738 39 #[clap(long)]40 whole_dist: bool,41 },42 43 Ensure {44 45 name: String,4647 48 #[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 131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 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}