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
1use std::{1use std::io::{Write as _, stdout};
2 collections::{BTreeSet, HashSet},
3 io::{Read, stdin},
4 path::PathBuf,
5};
62
7use anyhow::{Context as _, Result, anyhow, bail, ensure};3use anyhow::{Context as _, Result, anyhow, bail};
8use clap::Parser;4use clap::Parser;
9use fleet_base::{fleetdata::SecretOwner, host::Config, opts::FleetOpts};5use fleet_base::{fleetdata::SecretOwner, host::Config, opts::FleetOpts};
10use fleet_shared::SecretData;
11use itertools::{ExactlyOneError, Itertools as _};6use itertools::Itertools as _;
12use tokio::fs::read;
13use tracing::{info, warn};7use tracing::warn;
148
15#[derive(Parser)]9#[derive(Parser)]
16pub enum Secret {10pub enum Secret {
27 machine: Option<String>,21 machine: Option<String>,
2822
29 /// Which private secret part to read23 /// Which private secret part to read
24 /// If not specified - only one existing part is read
30 #[clap(short = 'p', long, default_value = "secret")]25 #[clap(short = 'p', long)]
31 part: Option<String>,26 part: Option<String>,
32
33 /// Which host should we use to decrypt, in case if reencryption is required, without
34 /// regeneration
35 #[clap(long)]
36 prefer_identities: Vec<String>,
37 },27 },
38 /// Prune (remove, mark for regeneration) secrets28 /// Prune (remove, mark for regeneration) secrets
39 Prune {29 Prune {
54 machine: Vec<String>,44 machine: Vec<String>,
55 },45 },
56 List {},46 List {},
57 Edit {
58 name: String,
59 #[clap(short = 'm', long)]
60 machine: String,
61
62 #[clap(long)]
63 add: bool,
64
65 /// Which private secret part to read
66 #[clap(short = 'p', long, default_value = "secret")]
67 part: String,
68 },
69}47}
7048
71impl Secret {49impl Secret {
83 name,61 name,
84 machine,62 machine,
85 part: part_name,63 part: part_name,
86 mut prefer_identities,
87 } => {64 } => {
65 let (owners, secret_data) = {
88 let secret = config.data.secrets.read().expect("not poisoned");66 let secret = config.data.secrets.read().expect("not poisoned");
8967
90 let Some(dist) = secret.get("name") else {68 let Some(dist) = secret.get(&name) else {
91 bail!("secret doesn't exists");69 bail!("secret doesn't exists");
92 };70 };
9371
103 )?81 )?
104 };82 };
10583
106 let part_name = part_name.unwrap_or_else(|| "secret".to_string());84 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.secret
90 .parts
91 .iter()
92 .exactly_one()
93 .map_err(|e| anyhow!("{e}"))
94 .context("with no part specified, there should be exactly one part")?
95 .1
96 };
107 let Some(part) = dist.secret.parts.get(&part_name) else {97 let owners = dist.owners().cloned().collect::<Vec<_>>();
98 let secret_data = part.raw.clone();
99 (owners, secret_data)
100 };
101
102 for host in config
103 .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 }
108 bail!("secret part {part_name:?} is not defined");119 Err(e) => warn!("failed to decrypt on {}: {e}", host.name),
109 };120 };
110121 }
111 // dist.get(SecretOwner(name));
112
113 todo!();122 bail!("failed to find suitable decrypting host");
114 /*
115 let Some(secret) = config.shared_secret(&name) else {
116 bail!("secret doesn't exists");
117 };
118
119 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 dist
128 } else {
129 bail!(
130 "secret {name} has shares, but no --machine specified for specifing which do you need"
131 )
132 };
133
134 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_identities
140 .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 }123 }
157 Secret::List {} => {124 Secret::List {} => {
158 /*125 /*
192 */159 */
193 todo!()160 todo!()
194 }161 }
195 Secret::Edit {
196 name,
197 machine,
198 part,
199 add,
200 } => {
201 /*let secret = config
202 .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!(),162 Secret::Prune { name, machine } => todo!(),
216 Secret::Ensure { name, machine } => todo!(),163 Secret::Ensure { name, machine } => todo!(),
217 }164 }
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
1use std::{1use std::{
2 collections::{BTreeMap, BTreeSet},2 collections::{BTreeMap, BTreeSet, HashSet},
3 ffi::{OsStr, OsString},3 ffi::{OsStr, OsString},
4 fmt::Display,4 fmt::Display,
5 io::Write,5 io::Write,
652 }652 }
653 }653 }
654
655 pub fn preferred_hosts(
656 &self,
657 filter: impl Fn(&str) -> bool,
658 ) -> Result<impl Iterator<Item = Result<ConfigHost>>> {
659 let prefer = self
660 .prefer_identities
661 .iter()
662 .filter_map(|v| v.as_host())
663 .collect::<HashSet<_>>();
664 let config = &self.config_field;
665 let mut names = nix_go!(config.hosts).list_fields()?;
666 names.retain(|s| filter(s));
667 names.sort_by_key(|h| prefer.contains(h.as_str()));
668
669 Ok(names.into_iter().map(|h| self.host(&h)))
670 }
654671
655 pub fn host(&self, name: &str) -> Result<ConfigHost> {672 pub fn host(&self, name: &str) -> Result<ConfigHost> {
656 let config = &self.config_field;673 let config = &self.config_field;