From eee08d567ee4f1c287d743f0e83cff58143cf2a8 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Fri, 09 Jun 2023 18:27:32 +0000 Subject: [PATCH] refactor: remote command management --- --- a/cmds/fleet/src/cmds/build_systems.rs +++ b/cmds/fleet/src/cmds/build_systems.rs @@ -1,9 +1,10 @@ -use std::{env::current_dir, process::Stdio, time::Duration}; +use std::{env::current_dir, time::Duration}; -use crate::{command::CommandExt, host::Config}; +use crate::command::MyCommand; +use crate::host::Config; use anyhow::Result; use clap::Parser; -use tokio::{process::Command, task::LocalSet, time::sleep}; +use tokio::{task::LocalSet, time::sleep}; use tracing::{error, field, info, info_span, warn, Instrument}; #[derive(Parser, Clone)] @@ -33,6 +34,9 @@ } pub(crate) fn should_switch_profile(&self) -> bool { + matches!(self, Self::Switch | Self::Boot) + } + pub(crate) fn should_activate(&self) -> bool { matches!(self, Self::Switch | Self::Test) } } @@ -108,13 +112,7 @@ dir.path().to_owned() }; - let mut nix_build = if self.privileged_build { - let mut out = Command::new("sudo"); - out.arg("nix"); - out - } else { - Command::new("nix") - }; + let mut nix_build = MyCommand::new("nix"); nix_build .args([ "build", @@ -122,9 +120,8 @@ "--json", // "--show-trace", "--no-link", - "--out-link", ]) - .arg(&built) + .comparg("--out-link", &built) .arg( config.configuration_attr_name(&format!( "buildSystems.{}.{host}", @@ -133,6 +130,10 @@ ) .args(&config.nix_args); + if self.privileged_build { + nix_build = nix_build.sudo(); + } + nix_build.run_nix().await.map_err(|e| { if action.build_attr() == "sdImage" { info!("sd-image build failed"); @@ -149,14 +150,11 @@ info!("uploading system closure"); let mut tries = 0; loop { - match Command::new("nix") - .args(["copy", "--to"]) - .arg(format!("ssh://root@{}", host)) - .arg(&built) - .inherit_stdio() - .run_nix() - .await - { + let mut nix = MyCommand::new("nix"); + nix.arg("copy") + .comparg("--to", format!("ssh://root@{host}")) + .arg(&built); + match nix.run_nix().await { Ok(()) => break, Err(e) if tries < 3 => { tries += 1; @@ -170,24 +168,20 @@ if let Some(action) = action { if action.should_switch_profile() { info!("switching generation"); - config - .command_on(&host, "nix-env", true) - .args(["-p", "/nix/var/nix/profiles/system", "--set"]) - .arg(&built) - .inherit_stdio() - .run() - .await?; + let mut cmd = MyCommand::new("nix-env"); + cmd.comparg("--profile", "/nix/var/nix/profiles/system") + .comparg("--set", &built); + config.run_on(&host, cmd, true).await?; + } + if action.should_activate() { + info!("executing activation script"); + let mut switch_script = built.clone(); + switch_script.push("bin"); + switch_script.push("switch-to-configuration"); + let mut cmd = MyCommand::new(switch_script); + cmd.arg(action.name()); + config.run_on(&host, cmd, true).await?; } - info!("executing activation script"); - let mut switch_script = built.clone(); - switch_script.push("bin"); - switch_script.push("switch-to-configuration"); - config - .command_on(&host, switch_script, true) - .arg(action.name()) - .stdout(Stdio::inherit()) - .run() - .await?; } } Action::Package(PackageAction::SdImage) => { @@ -195,39 +189,30 @@ out.push(format!("sd-image-{}", host)); info!("building sd image to {:?}", out); - let mut nix_build = if self.privileged_build { - let mut out = Command::new("sudo"); - out.arg("nix"); - out - } else { - Command::new("nix") - }; + let mut nix_build = MyCommand::new("nix"); nix_build - .args(["build", "--impure", "--no-link", "--out-link"]) - .arg(&out) + .args(["build", "--impure", "--no-link"]) + .comparg("--out-link", &out) .arg(config.configuration_attr_name(&format!("buildSystems.sdImage.{}", host,))) .args(&config.nix_args); if !self.fail_fast { nix_build.arg("--keep-going"); } + if self.privileged_build { + nix_build = nix_build.sudo(); + } - nix_build.inherit_stdio().run_nix().await?; + nix_build.run_nix().await?; } Action::Package(PackageAction::InstallationCd) => { let mut out = current_dir()?; out.push(format!("installation-cd-{}", host)); info!("building sd image to {:?}", out); - let mut nix_build = if self.privileged_build { - let mut out = Command::new("sudo"); - out.arg("nix"); - out - } else { - Command::new("nix") - }; + let mut nix_build = MyCommand::new("nix"); nix_build - .args(["build", "--impure", "--no-link", "--out-link"]) - .arg(&out) + .args(["build", "--impure", "--no-link"]) + .comparg("--out-link", &out) .arg( config.configuration_attr_name(&format!( "buildSystems.installationCd.{}", @@ -238,8 +223,11 @@ if !self.fail_fast { nix_build.arg("--keep-going"); } + if self.privileged_build { + nix_build = nix_build.sudo(); + } - nix_build.inherit_stdio().run_nix().await?; + nix_build.run_nix().await?; } }; Ok(()) --- a/cmds/fleet/src/cmds/secrets/mod.rs +++ b/cmds/fleet/src/cmds/secrets/mod.rs @@ -2,18 +2,16 @@ fleetdata::{FleetSecret, FleetSharedSecret}, host::Config, }; -use age::{Decryptor, Encryptor}; use anyhow::{bail, ensure, Context, Result}; use clap::Parser; use futures::{StreamExt, TryStreamExt}; use std::{ collections::HashSet, - io::{self, Cursor, Read, Write}, - iter, + io::{self, Cursor, Read}, path::PathBuf, }; use tokio::fs::read_to_string; -use tracing::{info, warn}; +use tracing::{info, warn, error}; #[derive(Parser)] pub enum Secrets { @@ -313,6 +311,7 @@ } let mut to_remove = Vec::new(); for name in &config.list_shared() { + info!("updating secret: {name}"); let mut data = config.shared_secret(name)?; let expected_owners: Vec = config .shared_config_attr(&format!("sharedSecrets.\"{name}\".expectedOwners")) @@ -326,14 +325,12 @@ let expected_set = expected_owners.iter().collect::>(); let should_remove = set.difference(&expected_set).next().is_some(); if set != expected_set { - warn!("reconfiguring owners for {name}"); - let generator: Option = config - .shared_config_attr(&format!("sharedSecrets.\"{name}\".generator")) + let owner_dependent: bool = config + .shared_config_attr(&format!("sharedSecrets.\"{name}\".ownerDependent")) .await?; - // TODO: if !.owner_dependent - if let Some(str) = generator { - todo!("regenerate") - } else { + if !owner_dependent { + warn!("reencrypting secret '{name}' for new owner set"); + // TODO: force regeneration if should_remove { warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"); } @@ -367,7 +364,16 @@ data.secret.secret = encrypted; data.owners = expected_owners; config.replace_shared(name.to_owned(), data); + } else if let Some(generator) = config + .shared_config_attr::>(&format!("sharedSecrets.\"{name}\".generator")) + .await? + { + todo!("regenerate secret {name} with {generator}"); + } else { + error!("secret '{name}' should be regenerated manually"); } + } else { + info!("secret data is ok") } } for k in to_remove { --- a/cmds/fleet/src/command.rs +++ b/cmds/fleet/src/command.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsStr, process::Stdio}; +use std::{ffi::OsStr, process::Stdio, task::Poll}; use anyhow::{Context, Result}; use async_trait::async_trait; @@ -7,20 +7,292 @@ de::{DeserializeOwned, Visitor}, Deserialize, }; -use tokio::{process::Command, select}; +use tokio::{io::AsyncRead, process::Command, select}; use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec}; use tracing::{info, warn}; +fn escape_bash(input: &str, out: &mut String) { + const TO_ESCAPE: &str = "$ !\"#&'()*,;<>?[\\]^`{|}"; + if input.chars().all(|c| !TO_ESCAPE.contains(c)) { + out.push_str(input); + return; + } + out.push('\''); + for (i, v) in input.split('\'').enumerate() { + if i != 0 { + out.push_str("'\"'\"'"); + } + out.push_str(v); + } + out.push('\''); +} +fn ostoutf8(os: impl AsRef) -> String { + os.as_ref().to_str().expect("non-utf8 data").to_owned() +} +#[derive(Clone)] +pub struct MyCommand { + command: String, + args: Vec, + env: Vec<(String, String)>, +} +impl MyCommand { + pub fn new(cmd: impl AsRef) -> Self { + assert!(!cmd.as_ref().is_empty()); + Self { + command: ostoutf8(cmd), + args: vec![], + env: vec![], + } + } + fn into_args(self) -> Vec { + let mut out = Vec::new(); + if !self.env.is_empty() { + out.push("env".to_owned()); + for (k, v) in self.env { + assert!(!k.contains("=")); + out.push(format!("{k}={v}")); + } + } + out.push(self.command); + out.extend(self.args.into_iter()); + out + } + fn into_string(self) -> String { + let mut out = String::new(); + if !self.env.is_empty() { + out.push_str("env"); + for (k, v) in self.env { + out.push(' '); + assert!(!k.contains("=")); + escape_bash(&k, &mut out); + out.push('='); + escape_bash(&v, &mut out); + } + } + if !out.is_empty() { + out.push(' '); + } + escape_bash(&self.command, &mut out); + for arg in self.args { + out.push(' '); + escape_bash(&arg, &mut out); + } + out + } + fn into_command(self) -> Command { + let mut out = Command::new(self.command); + out.args(self.args); + for (k, v) in self.env { + out.env(k, v); + } + out + } + pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { + let arg = arg.as_ref(); + self.args.push(ostoutf8(arg)); + self + } + pub fn eqarg(&mut self, arg: impl AsRef, value: impl AsRef) -> &mut Self { + let arg = arg.as_ref(); + let value = value.as_ref(); + let arg = ostoutf8(arg); + let value = ostoutf8(value); + self.arg(format!("{arg}={value}")); + self + } + pub fn comparg(&mut self, arg: impl AsRef, value: impl AsRef) -> &mut Self { + self.arg(arg); + self.arg(value); + self + } + pub fn args>(&mut self, args: impl IntoIterator) -> &mut Self { + for arg in args.into_iter() { + let arg = arg.as_ref(); + self.args.push(ostoutf8(arg)); + } + self + } + pub fn sudo(self) -> Self { + let mut out = Self::new("sudo"); + out.args(self.into_args()); + out + } + pub fn ssh(self, on: impl AsRef) -> Self { + let mut out = Self::new("ssh"); + out.arg(on).arg("--"); + out.arg(self.into_string()); + out + } + + pub async fn run(self) -> Result<()> { + let str = self.clone().into_string(); + info!("running {str}"); + let mut cmd = self.into_command(); + cmd.inherit_stdio(); + let out = cmd.spawn()?.wait_with_output().await?; + if !out.status.success() { + anyhow::bail!("command '{}' failed with status {}", str, out.status); + } + Ok(()) + } + pub async fn run_string(self) -> Result { + let str = self.clone().into_string(); + info!("running {str}"); + let mut cmd = self.into_command(); + cmd.inherit_stdio(); + cmd.stdout(Stdio::piped()); + let out = cmd.spawn()?.wait_with_output().await?; + if !out.status.success() { + anyhow::bail!("command '{}' failed with status {}", str, out.status); + } + Ok(String::from_utf8(out.stdout)?) + } + pub async fn run_nix_json(self) -> Result { + let str = self.run_nix_string().await?; + serde_json::from_str(&str).with_context(|| format!("{:?}", str)) + } + + pub async fn run_nix_string(self) -> Result { + let str = self.clone().into_string(); + let mut cmd = self.into_command(); + cmd.stdout(Stdio::piped()); + run_nix_inner(str, cmd).await.map(|v| v.unwrap()) + } + pub async fn run_nix(self) -> Result<()> { + let str = self.clone().into_string(); + let mut cmd = self.into_command(); + cmd.stdout(Stdio::inherit()); + run_nix_inner(str, cmd).await.map(|v| { + assert!(v.is_none()); + }) + } +} + +struct EmptyAsyncRead; +impl AsyncRead for EmptyAsyncRead { + fn poll_read( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + _buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + Poll::Pending + } +} + +async fn run_nix_inner(str: String, mut cmd: Command) -> Result> { + info!("running {str}"); + cmd.arg("--log-format").arg("internal-json"); + cmd.stderr(Stdio::piped()); + let mut child = cmd.spawn()?; + let mut stderr = child.stderr.take().unwrap(); + let stdout = child.stdout.take(); + let wants_stdout = stdout.is_some(); + let mut err = FramedRead::new(&mut stderr, LinesCodec::new()); + let mut out: Box = stdout + .map(|s| Box::new(s) as Box) + .unwrap_or_else(|| Box::new(EmptyAsyncRead)); + let mut out = FramedRead::new(&mut out, BytesCodec::new()); + + // while let Some(line) = read.next().await? {} + + let mut out_buf = if wants_stdout { Some(vec![]) } else { None }; + loop { + select! { + e = err.next() => { + if let Some(e) = e { + let e = e?; + if let Some(e) = e.strip_prefix("@nix ") { + + let log: NixLog = match serde_json::from_str(e) { + Ok(l) => l, + Err(err) => { + warn!("failed to parse nix log line {:?}: {}", e, err); + continue; + }, + }; + match log { + NixLog::Msg { msg, raw_msg, .. } => { + if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '") && msg.ends_with("' is dirty")) + && !msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake") + && msg != "\u{1b}[35;1mwarning:\u{1b}[0m \u{1b}[31;1merror:\u{1b}[0m SQLite database '\u{1b}[35;1m/nix/var/nix/db/db.sqlite\u{1b}[0m' is busy" { + if let Some(raw_msg) = raw_msg { + info!(target: "nix", "{raw_msg}\n{msg}") + }else { + info!(target: "nix", "{msg}") + + } + } + }, + NixLog::Start { ref fields, typ, .. } if typ == 105 && !fields.is_empty() => { + if let [LogField::String(drv), ..] = &fields[..] { + info!(target: "nix","building {}", drv) + } else { + warn!("bad build log: {:?}", log) + } + }, + NixLog::Start { ref fields, typ, .. } if typ == 100 && fields.len() >= 3 => { + if let [LogField::String(drv), LogField::String(from), LogField::String(to), ..] = &fields[..] { + info!(target: "nix","copying {} {} -> {}", drv, from, to) + } else { + warn!("bad copy log: {:?}", log) + } + }, + NixLog::Start { text, typ, .. } if typ == 0 || typ == 102 || typ == 103 || typ == 104 => { + if !text.is_empty() && text != "querying info about missing paths" && text != "copying 0 paths" { + info!(target: "nix", "{}", text) + } + }, + NixLog::Start { text, level: 0, typ: 108, .. } if text.is_empty() => { + // Cache lookup? Coupled with copy log + }, + NixLog::Start { text, level: 4, typ: 109, .. } if text.starts_with("querying info about ") => { + // Cache lookup + } + NixLog::Start { text, level: 4, typ: 101, .. } if text.starts_with("downloading ") => { + // NAR downloading, coupled with copy log + } + NixLog::Start { text, level: 1, typ: 111, .. } if text.starts_with("waiting for a machine to build ") => { + // Useless repeating notification about build + } + NixLog::Start { text, level: 3, typ: 111, .. } if text.starts_with("resolved derivation: ") => { + // CA resolved + } + NixLog::Stop { .. } => {}, + NixLog::Result { .. } => {}, + _ => warn!("unknown log: {:?}", log) + }; + } else { + warn!(target="nix","unknown: {}", e) + } + } + }, + o = out.next() => { + if let Some(o) = o { + out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?); + } + }, + code = child.wait() => { + let code = code?; + if !code.success() { + anyhow::bail!("command '{str}' failed with status {}", code); + } + break; + } + } + } + + Ok(out_buf.map(String::from_utf8).transpose()?) +} + #[async_trait] pub trait CommandExt { - async fn run_nix(&mut self) -> Result<()>; - async fn run_nix_json(&mut self) -> Result; - async fn run_nix_string(&mut self) -> Result; - async fn run(&mut self) -> Result<()>; - async fn run_json(&mut self) -> Result; - async fn run_string(&mut self) -> Result; + // async fn run_nix(&mut self) -> Result<()>; + // async fn run_nix_json(&mut self) -> Result; + // async fn run_nix_string(&mut self) -> Result; + // async fn run(&mut self) -> Result<()>; + // async fn run_json(&mut self) -> Result; + // async fn run_string(&mut self) -> Result; fn inherit_stdio(&mut self) -> &mut Self; - fn ssh_on(host: impl AsRef, command: impl AsRef) -> Self; } #[derive(Debug)] @@ -91,170 +363,9 @@ #[async_trait] impl CommandExt for Command { - async fn run_nix(&mut self) -> Result<()> { - self.run_nix_string().await.map(|_| ()) - } - async fn run_nix_json(&mut self) -> Result { - let str = self.run_nix_string().await?; - serde_json::from_str(&str).with_context(|| format!("{:?}", str)) - } - - async fn run_nix_string(&mut self) -> Result { - self.arg("--log-format").arg("internal-json"); - self.stderr(Stdio::piped()); - self.stdout(Stdio::piped()); - let mut child = self.spawn()?; - let mut stderr = child.stderr.take().unwrap(); - let mut stdout = child.stdout.take().unwrap(); - let mut err = FramedRead::new(&mut stderr, LinesCodec::new()); - let mut out = FramedRead::new(&mut stdout, BytesCodec::new()); - - // while let Some(line) = read.next().await? {} - - let mut out_buf = vec![]; - loop { - select! { - e = err.next() => { - if let Some(e) = e { - let e = e?; - if let Some(e) = e.strip_prefix("@nix ") { - - let log: NixLog = match serde_json::from_str(e) { - Ok(l) => l, - Err(err) => { - warn!("failed to parse nix log line {:?}: {}", e, err); - continue; - }, - }; - match log { - NixLog::Msg { msg, raw_msg, .. } => { - if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '") && msg.ends_with("' is dirty")) - && !msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake") - && msg != "\u{1b}[35;1mwarning:\u{1b}[0m \u{1b}[31;1merror:\u{1b}[0m SQLite database '\u{1b}[35;1m/nix/var/nix/db/db.sqlite\u{1b}[0m' is busy" { - if let Some(raw_msg) = raw_msg { - info!(target: "nix", "{raw_msg}\n{msg}") - }else { - info!(target: "nix", "{msg}") - - } - } - }, - NixLog::Start { ref fields, typ, .. } if typ == 105 && !fields.is_empty() => { - if let [LogField::String(drv), ..] = &fields[..] { - info!(target: "nix","building {}", drv) - } else { - warn!("bad build log: {:?}", log) - } - }, - NixLog::Start { ref fields, typ, .. } if typ == 100 && fields.len() >= 3 => { - if let [LogField::String(drv), LogField::String(from), LogField::String(to), ..] = &fields[..] { - info!(target: "nix","copying {} {} -> {}", drv, from, to) - } else { - warn!("bad copy log: {:?}", log) - } - }, - NixLog::Start { text, typ, .. } if typ == 0 || typ == 102 || typ == 103 || typ == 104 => { - if !text.is_empty() && text != "querying info about missing paths" && text != "copying 0 paths" { - info!(target: "nix", "{}", text) - } - }, - NixLog::Start { text, level: 0, typ: 108, .. } if text.is_empty() => { - // Cache lookup? Coupled with copy log - }, - NixLog::Start { text, level: 4, typ: 109, .. } if text.starts_with("querying info about ") => { - // Cache lookup - } - NixLog::Start { text, level: 4, typ: 101, .. } if text.starts_with("downloading ") => { - // NAR downloading, coupled with copy log - } - NixLog::Start { text, level: 1, typ: 111, .. } if text.starts_with("waiting for a machine to build ") => { - // Useless repeating notification about build - } - NixLog::Start { text, level: 3, typ: 111, .. } if text.starts_with("resolved derivation: ") => { - // CA resolved - } - NixLog::Stop { .. } => {}, - NixLog::Result { .. } => {}, - _ => warn!("unknown log: {:?}", log) - }; - } else { - warn!(target="nix","unknown: {}", e) - } - } - }, - o = out.next() => { - if let Some(o) = o { - out_buf.extend_from_slice(&o?); - } - }, - code = child.wait() => { - let code = code?; - if !code.success() { - anyhow::bail!("command ({:?}) failed with status {}", self, code); - } - break; - } - } - } - - Ok(String::from_utf8(out_buf)?) - } - fn inherit_stdio(&mut self) -> &mut Self { self.stderr(Stdio::inherit()); + self.stdout(Stdio::inherit()); self - } - - async fn run(&mut self) -> Result<()> { - self.stderr(Stdio::piped()); - self.stdout(Stdio::piped()); - let mut child = self.spawn()?; - let mut stderr = child.stderr.take().unwrap(); - let mut stdout = child.stdout.take().unwrap(); - let mut err = FramedRead::new(&mut stderr, LinesCodec::new()); - let mut out = FramedRead::new(&mut stdout, LinesCodec::new()); - loop { - select! { - e = err.next() => { - if let Some(e) = e { - warn!("{}", e?); - } - }, - o = out.next() => { - if let Some(o) = o { - info!("{}", o?); - } - }, - code = child.wait() => { - let code = code?; - if !code.success() { - anyhow::bail!("command ({:?}) failed with status {}", self, code); - } - break; - } - } - } - Ok(()) - } - - async fn run_json(&mut self) -> Result { - let str = self.run_string().await?; - serde_json::from_str(&str).with_context(|| format!("{:?}", str)) - } - - async fn run_string(&mut self) -> Result { - self.inherit_stdio(); - self.stdout(Stdio::piped()); - let out = self.spawn()?.wait_with_output().await?; - if !out.status.success() { - anyhow::bail!("command ({:?}) failed with status {}", self, out.status); - } - Ok(String::from_utf8(out.stdout)?) - } - - fn ssh_on(host: impl AsRef, command: impl AsRef) -> Self { - let mut cmd = Command::new("ssh"); - cmd.arg(host).arg("--").arg(command); - cmd } } --- a/cmds/fleet/src/host.rs +++ b/cmds/fleet/src/host.rs @@ -1,7 +1,7 @@ use std::{ cell::{Ref, RefCell, RefMut}, env::current_dir, - ffi::{OsStr, OsString}, + ffi::OsString, io::Write, ops::Deref, path::PathBuf, @@ -12,10 +12,9 @@ use clap::{ArgGroup, Parser}; use serde::de::DeserializeOwned; use tempfile::NamedTempFile; -use tokio::process::Command; use crate::{ - command::CommandExt, + command::MyCommand, fleetdata::{FleetData, FleetSecret, FleetSharedSecret}, }; @@ -52,24 +51,24 @@ self.opts.localhost.as_ref().map(|s| s as &str) == Some(host) } - pub fn command_on(&self, host: &str, program: impl AsRef, sudo: bool) -> Command { - if self.is_local(host) { - if sudo { - let mut cmd = Command::new("sudo"); - cmd.arg(program); - cmd - } else { - Command::new(program) - } - } else { - let mut cmd = Command::new("ssh"); - cmd.arg(host).arg("--"); - if sudo { - cmd.arg("sudo"); - } - cmd.arg(program); - cmd + pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> { + if sudo { + command = command.sudo(); + } + if !self.is_local(host) { + command = command.ssh(host); + } + command.run().await + } + #[must_use] + pub async fn run_string_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result { + if sudo { + command = command.sudo(); + } + if !self.is_local(host) { + command = command.ssh(host); } + command.run_string().await } pub fn configuration_attr_name(&self, name: &str) -> OsString { @@ -83,36 +82,36 @@ } pub async fn list_hosts(&self) -> Result> { - Command::new("nix") - .arg("eval") + let mut cmd = MyCommand::new("nix"); + cmd.arg("eval") .arg(self.configuration_attr_name("configuredHosts")) .args(["--apply", "builtins.attrNames", "--json", "--show-trace"]) - .args(&self.nix_args) - .run_nix_json() + .args(&self.nix_args); + cmd.run_nix_json() .await } pub async fn shared_config_attr(&self, attr: &str) -> Result { - Command::new("nix") - .arg("eval") + let mut cmd = MyCommand::new("nix"); + cmd.arg("eval") .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr))) .args(["--json", "--show-trace"]) - .args(&self.nix_args) - .run_nix_json() + .args(&self.nix_args); + cmd.run_nix_json() .await } pub async fn shared_config_attr_names(&self, attr: &str) -> Result> { - Command::new("nix") - .arg("eval") + let mut cmd = MyCommand::new("nix"); + cmd.arg("eval") .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr))) .args(["--apply", "builtins.attrNames"]) .args(["--json", "--show-trace"]) - .args(&self.nix_args) - .run_nix_json() + .args(&self.nix_args); + cmd.run_nix_json() .await } pub async fn config_attr(&self, host: &str, attr: &str) -> Result { - Command::new("nix") - .arg("eval") + let mut cmd = MyCommand::new("nix"); + cmd.arg("eval") .arg( self.configuration_attr_name(&format!( "configuredSystems.{}.config.{}", @@ -120,8 +119,8 @@ )), ) .args(["--json", "--show-trace"]) - .args(&self.nix_args) - .run_nix_json() + .args(&self.nix_args); + cmd.run_nix_json() .await } @@ -171,23 +170,20 @@ pub async fn decrypt_on_host(&self, host: &str, data: Vec) -> Result>{ let data = z85::encode(&data); - let encoded = self.command_on(host, "fleet-install-secrets", true) - .arg("decrypt") - .arg("--secret") - .arg(data).run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned(); + let mut cmd = MyCommand::new("fleet-install-secrets"); + cmd.arg("decrypt").eqarg("--secret", data); + cmd = cmd.sudo().ssh(host); + let encoded = cmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned(); Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?) } pub async fn reencrypt_on_host(&self, host: &str, data: Vec, targets: Vec) -> Result>{ let data = z85::encode(&data); - let mut recmd = self.command_on(host, "fleet-install-secrets", true); - recmd - .arg("reencrypt") - .arg("--secret") - .arg(format!("\"{}\"", data.replace('$', "\\$"))); + let mut recmd = MyCommand::new("fleet-install-secrets"); + recmd.arg("reencrypt").eqarg("--secret",data); for target in targets { - recmd.arg("--targets"); - recmd.arg(format!("\"{target}\"")); + recmd.eqarg("--targets", target); } + recmd = recmd.sudo().ssh(host); let encoded = recmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned(); Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?) } --- a/cmds/fleet/src/keys.rs +++ b/cmds/fleet/src/keys.rs @@ -1,6 +1,7 @@ use std::str::FromStr; -use crate::{command::CommandExt, host::Config}; +use crate::command::MyCommand; +use crate::host::Config; use anyhow::{anyhow, Result}; use tracing::warn; @@ -26,11 +27,9 @@ Ok(key) } else { warn!("Loading key for {}", host); - let key = self - .command_on(host, "cat", false) - .arg("/etc/ssh/ssh_host_ed25519_key.pub") - .run_string() - .await?; + let mut cmd = MyCommand::new("cat"); + cmd.arg("/etc/ssh/ssh_host_ed25519_key.pub"); + let key = self.run_string_on(host, cmd, false).await?; self.update_key(host, key.clone()); Ok(key) } --- a/cmds/install-secrets/src/main.rs +++ b/cmds/install-secrets/src/main.rs @@ -250,7 +250,7 @@ if plaintext { let s = String::from_utf8(decrypted).context("output is not utf8")?; - print!("{}", s); + print!("{s}"); } else { println!("{}", SecretWrapper(decrypted)); } --- a/modules/fleet/secrets.nix +++ b/modules/fleet/secrets.nix @@ -2,15 +2,6 @@ let sharedSecret = with types; { options = { - owners = mkOption { - type = listOf str; - description = '' - For which owners this secret is currently encrypted, - if not matches expectedOwners - then this secret is considered outdated, and - should be regenerated/reencrypted - ''; - default = [ ]; - }; expectedOwners = mkOption { type = listOf str; description = '' @@ -25,12 +16,38 @@ description = "Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted"; }; generator = mkOption { - type = nullOr package; - description = '' - Derivation to execute for secret generation + type = nullOr (submodule { + packages = mkOption { + type = attrsOf package; + description = '' + Derivation to execute for shared secret generation (key = system). + This derivation should produce directory, with exactly two files: + - publicData + - encryptedSecretData + + If null - secret value may only be created manually. + ''; + }; + expectedData = mkOption { + type = types.unspecified; + description = "Data expected to be used for secret generation, if doesn't match specified - secret should be regenerated"; + }; + dependencies = mkOption { + type = listOf str; + description = '' + List of secrets, on which this secret depends. - If null - may only be created manually - ''; + During generation, generator command will be ran on host, which already has specified secrets generated. + ''; + default = []; + }; + data = mkOption { + type = types.unspecified; + description = "Data used for secret generation. Imported from fleet.nix"; + default = null; + internal = true; + }; + }); default = null; }; expireIn = mkOption { @@ -38,15 +55,28 @@ description = "Time in hours, in which this secret should be regenerated"; default = null; }; + + owners = mkOption { + type = listOf str; + description = '' + For which owners this secret is currently encrypted, + if not matches expectedOwners - then this secret is considered outdated, and + should be regenerated/reencrypted. + + Imported from fleet.nix + ''; + default = [ ]; + }; public = mkOption { type = nullOr str; - description = "Secret public data"; + description = "Secret public data. Imported from fleet.nix"; default = null; }; secret = mkOption { type = nullOr str; - description = "Encrypted secret data"; + description = "Encrypted secret data. Imported from fleet.nix"; default = null; + internal = true; }; }; }; --- a/nixos/secrets.nix +++ b/nixos/secrets.nix @@ -5,14 +5,14 @@ let sysConfig = config; secretType = types.submodule ({ config, ... }: { - config = rec { - stableSecretPath = mkOptionDefault "/run/secrets/secret-stable-${config._module.args.name}"; - secretPath = mkOptionDefault "/run/secrets/secret-${config.secretHash}-${config._module.args.name}"; - secretHash = mkOptionDefault (if config.secret != null then (builtins.hashString "sha1" config.secret) else ""); + config = let secretName = config._module.args.name; in rec { + stableSecretPath = mkOptionDefault "/run/secrets/secret-stable-${secretName}"; + secretPath = mkOptionDefault "/run/secrets/secret-${config.secretHash}-${secretName}"; + secretHash = mkOptionDefault (if config.secret != null then (builtins.hashString "sha1" config.secret) else throw "secret is not defined for secret ${secretName}"); - stablePublicPath = mkOptionDefault "/run/secrets/public-stable-${config._module.args.name}"; - publicPath = mkOptionDefault "/run/secrets/public-${config.publicHash}-${config._module.args.name}"; - publicHash = mkOptionDefault (if config.public != null then (builtins.hashString "sha1" config.public) else ""); + stablePublicPath = mkOptionDefault "/run/secrets/public-stable-${secretName}"; + publicPath = mkOptionDefault "/run/secrets/public-${config.publicHash}-${secretName}"; + publicHash = mkOptionDefault (if config.public != null then (builtins.hashString "sha1" config.public) else throw "public is not defined for secret ${secretName}"); }; options = { public = mkOption { @@ -77,7 +77,13 @@ }); secretsFile = pkgs.writeTextFile { name = "secrets.json"; - text = builtins.toJSON config.secrets; + text = builtins.toJSON (mapAttrs (_: value: rec { + inherit (value) group mode owner secret public; + publicPath = if public != null then value.publicPath else "/missingno"; + stablePublicPath = if public != null then value.stablePublicPath else "/missingno"; + secretPath = if secret != null then value.secretPath else "/missingno"; + stableSecretPath = if secret != null then value.stableSecretPath else "/missingno"; + }) config.secrets); }; in { -- gitstuff