difftreelog
feat fleet secret read command reimplementation
in: trunk
2 files changed
cmds/fleet/src/cmds/secrets.rsdiffbeforeafterboth1use std::{2 collections::{BTreeSet, HashSet},3 io::{Read, stdin},4 path::PathBuf,5};67use anyhow::{Context as _, Result, anyhow, bail, ensure};8use clap::Parser;9use fleet_base::{fleetdata::SecretOwner, host::Config, opts::FleetOpts};10use fleet_shared::SecretData;11use itertools::{ExactlyOneError, Itertools as _};12use tokio::fs::read;13use tracing::{info, warn};1415#[derive(Parser)]16pub enum Secret {17 /// Force load host keys for all defined hosts18 ForceKeys,19 /// Read secret from remote host, requires sudo on one of the owning hosts20 Read {21 /// Secret name to read22 name: String,2324 /// Distribution with what machine to read25 /// If not shared between multiple - defaults to single owner26 #[clap(short = 'm', long)]27 machine: Option<String>,2829 /// Which private secret part to read30 #[clap(short = 'p', long, default_value = "secret")]31 part: Option<String>,3233 /// Which host should we use to decrypt, in case if reencryption is required, without34 /// regeneration35 #[clap(long)]36 prefer_identities: Vec<String>,37 },38 /// Prune (remove, mark for regeneration) secrets39 Prune {40 /// Secret to prune41 name: String,4243 /// Machines to prune - if specified, only the choosen machines will be pruned44 #[clap(short = 'm', long)]45 machine: Vec<String>,46 },47 /// Ensure secret is generated and not expired48 Ensure {49 /// Secret to ensure generated50 name: String,5152 /// Machines to force secret for53 #[clap(short = 'm', long)]54 machine: Vec<String>,55 },56 List {},57 Edit {58 name: String,59 #[clap(short = 'm', long)]60 machine: String,6162 #[clap(long)]63 add: bool,6465 /// Which private secret part to read66 #[clap(short = 'p', long, default_value = "secret")]67 part: String,68 },69}7071impl Secret {72 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {73 match self {74 Secret::ForceKeys => {75 for host in config.list_hosts()? {76 if opts.should_skip(&host)? {77 continue;78 }79 config.host_key(&host.name).await?;80 }81 }82 Secret::Read {83 name,84 machine,85 part: part_name,86 mut prefer_identities,87 } => {88 let secret = config.data.secrets.read().expect("not poisoned");8990 let Some(dist) = secret.get("name") else {91 bail!("secret doesn't exists");92 };9394 let dist = if let Some(machine) = &machine {95 dist.get(&SecretOwner::host(machine))96 .ok_or_else(|| anyhow!("machine {machine} has no secret generated"))?97 } else {98 dist.distributions()99 .exactly_one()100 .map_err(|e| anyhow!("{e}"))101 .context(102 "with no machine specified, there should be exactly one distribution",103 )?104 };105106 let part_name = part_name.unwrap_or_else(|| "secret".to_string());107 let Some(part) = dist.secret.parts.get(&part_name) else {108 bail!("secret part {part_name:?} is not defined");109 };110111 // dist.get(SecretOwner(name));112113 todo!();114 /*115 let Some(secret) = config.shared_secret(&name) else {116 bail!("secret doesn't exists");117 };118119 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 dist128 } else {129 bail!(130 "secret {name} has shares, but no --machine specified for specifing which do you need"131 )132 };133134 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_identities140 .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 }157 Secret::List {} => {158 /*159 let _span = info_span!("loading secrets").entered();160 let configured = config.list_configured_shared()?;161 #[derive(Tabled)]162 struct SecretDisplay {163 #[tabled(rename = "Name")]164 name: String,165 #[tabled(rename = "Owners")]166 owners: String,167 }168 // let mut table = vec![];169 for name in configured.iter().cloned() {170 let config = config.clone();171 let data = config.shared_secret(&name).expect("exists");172 /*173 let definition = config.shared_secret_definition(&name)?;174 let expectations = definition.expectations()?;175 let owners = data176 .owners()177 .map(|o| {178 if expectations.owners.contains(o) {179 o.green().to_string()180 } else {181 o.red().to_string()182 }183 })184 .collect::<Vec<_>>();185 table.push(SecretDisplay {186 owners: owners.join(", "),187 name,188 })189 */190 }191 // info!("loaded\n{}", Table::new(table).to_string())192 */193 todo!()194 }195 Secret::Edit {196 name,197 machine,198 part,199 add,200 } => {201 /*let secret = config202 .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!(),216 Secret::Ensure { name, machine } => todo!(),217 }218 Ok(())219 }220}221222/*223async fn edit_temp_file(224 builder: tempfile::Builder<'_, '_>,225 r: Vec<u8>,226 header: &str,227 comment: &str,228) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {229 if !stdin().is_tty() {230 // TODO: Also try to open /dev/tty directly?231 bail!("stdin is not tty, can't open editor");232 }233234 use std::fmt::Write;235 let mut file = builder.tempfile()?;236237 let mut full_header = String::new();238 let mut had = false;239 for line in header.trim_end().lines() {240 had = true;241 writeln!(&mut full_header, "{comment}{line}")?;242 }243 if had {244 writeln!(&mut full_header, "{}", comment.trim_end())?;245 }246 writeln!(247 &mut full_header,248 "{comment}Do not touch this header! It will be removed automatically"249 )?;250251 file.write_all(full_header.as_bytes())?;252 file.write_all(&r)?;253254 let abs_path = file.into_temp_path();255 let editor = std::env::var_os("VISUAL")256 .or_else(|| std::env::var_os("EDITOR"))257 .unwrap_or_else(|| "vi".into());258 let editor_args = shlex::bytes::split(editor.as_encoded_bytes())259 .ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;260 let editor_args = editor_args261 .into_iter()262 .map(|v| {263 // Only ASCII subsequences are replaced264 unsafe { OsString::from_encoded_bytes_unchecked(v) }265 })266 .collect_vec();267 let Some((editor, args)) = editor_args.split_first() else {268 bail!("EDITOR env var has no command");269 };270 let mut command = Command::new(editor);271 command.args(args);272273 let path_arg = abs_path.canonicalize()?;274275 // TODO: Save full state, using tcget/_getmode/_setmode276 let was_raw = terminal::is_raw_mode_enabled()?;277 terminal::enable_raw_mode()?;278279 let status = command.arg(path_arg).status().await;280281 if !was_raw {282 terminal::disable_raw_mode()?;283 }284285 let success = match status {286 Ok(s) => s.success(),287 Err(e) if e.kind() == io::ErrorKind::NotFound => {288 bail!("editor not found")289 }290 Err(e) => bail!("editor spawn error: {e}"),291 };292293 let mut file = std::fs::read(&abs_path).context("read editor output")?;294 let Some(v) = file.strip_prefix(full_header.as_bytes()) else {295 todo!();296 };297 todo!();298299 // Ok((success, abs_path))300}301*/crates/fleet-base/src/host.rsdiffbeforeafterboth--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -1,5 +1,5 @@
use std::{
- collections::{BTreeMap, BTreeSet},
+ collections::{BTreeMap, BTreeSet, HashSet},
ffi::{OsStr, OsString},
fmt::Display,
io::Write,
@@ -652,6 +652,23 @@
}
}
+ pub fn preferred_hosts(
+ &self,
+ filter: impl Fn(&str) -> bool,
+ ) -> Result<impl Iterator<Item = Result<ConfigHost>>> {
+ let prefer = self
+ .prefer_identities
+ .iter()
+ .filter_map(|v| v.as_host())
+ .collect::<HashSet<_>>();
+ let config = &self.config_field;
+ let mut names = nix_go!(config.hosts).list_fields()?;
+ names.retain(|s| filter(s));
+ names.sort_by_key(|h| prefer.contains(h.as_str()));
+
+ Ok(names.into_iter().map(|h| self.host(&h)))
+ }
+
pub fn host(&self, name: &str) -> Result<ConfigHost> {
let config = &self.config_field;
let host_config = nix_go!(config.hosts[{ name }]);