difftreelog
refactor remote command management
in: trunk
8 files changed
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- 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(())
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- 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<String> = config
.shared_config_attr(&format!("sharedSecrets.\"{name}\".expectedOwners"))
@@ -326,14 +325,12 @@
let expected_set = expected_owners.iter().collect::<HashSet<_>>();
let should_remove = set.difference(&expected_set).next().is_some();
if set != expected_set {
- warn!("reconfiguring owners for {name}");
- let generator: Option<String> = 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::<Option<String>>(&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 {
cmds/fleet/src/command.rsdiffbeforeafterboth--- 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<OsStr>) -> String {
+ os.as_ref().to_str().expect("non-utf8 data").to_owned()
+}
+#[derive(Clone)]
+pub struct MyCommand {
+ command: String,
+ args: Vec<String>,
+ env: Vec<(String, String)>,
+}
+impl MyCommand {
+ pub fn new(cmd: impl AsRef<OsStr>) -> Self {
+ assert!(!cmd.as_ref().is_empty());
+ Self {
+ command: ostoutf8(cmd),
+ args: vec![],
+ env: vec![],
+ }
+ }
+ fn into_args(self) -> Vec<String> {
+ 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<OsStr>) -> &mut Self {
+ let arg = arg.as_ref();
+ self.args.push(ostoutf8(arg));
+ self
+ }
+ pub fn eqarg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &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<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
+ self.arg(arg);
+ self.arg(value);
+ self
+ }
+ pub fn args<V: AsRef<OsStr>>(&mut self, args: impl IntoIterator<Item = V>) -> &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<OsStr>) -> 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<String> {
+ 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<T: DeserializeOwned>(self) -> Result<T> {
+ let str = self.run_nix_string().await?;
+ serde_json::from_str(&str).with_context(|| format!("{:?}", str))
+ }
+
+ pub async fn run_nix_string(self) -> Result<String> {
+ 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<std::io::Result<()>> {
+ Poll::Pending
+ }
+}
+
+async fn run_nix_inner(str: String, mut cmd: Command) -> Result<Option<String>> {
+ 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<dyn AsyncRead + Unpin> = stdout
+ .map(|s| Box::new(s) as Box<dyn AsyncRead + Unpin>)
+ .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<T: DeserializeOwned>(&mut self) -> Result<T>;
- async fn run_nix_string(&mut self) -> Result<String>;
- async fn run(&mut self) -> Result<()>;
- async fn run_json<T: DeserializeOwned>(&mut self) -> Result<T>;
- async fn run_string(&mut self) -> Result<String>;
+ // async fn run_nix(&mut self) -> Result<()>;
+ // async fn run_nix_json<T: DeserializeOwned>(&mut self) -> Result<T>;
+ // async fn run_nix_string(&mut self) -> Result<String>;
+ // async fn run(&mut self) -> Result<()>;
+ // async fn run_json<T: DeserializeOwned>(&mut self) -> Result<T>;
+ // async fn run_string(&mut self) -> Result<String>;
fn inherit_stdio(&mut self) -> &mut Self;
- fn ssh_on(host: impl AsRef<OsStr>, command: impl AsRef<OsStr>) -> 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<T: DeserializeOwned>(&mut self) -> Result<T> {
- let str = self.run_nix_string().await?;
- serde_json::from_str(&str).with_context(|| format!("{:?}", str))
- }
-
- async fn run_nix_string(&mut self) -> Result<String> {
- 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<T: DeserializeOwned>(&mut self) -> Result<T> {
- let str = self.run_string().await?;
- serde_json::from_str(&str).with_context(|| format!("{:?}", str))
- }
-
- async fn run_string(&mut self) -> Result<String> {
- 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<OsStr>, command: impl AsRef<OsStr>) -> Self {
- let mut cmd = Command::new("ssh");
- cmd.arg(host).arg("--").arg(command);
- cmd
}
}
cmds/fleet/src/host.rsdiffbeforeafterboth1use std::{2 cell::{Ref, RefCell, RefMut},3 env::current_dir,4 ffi::OsString,5 io::Write,6 ops::Deref,7 path::PathBuf,8 sync::Arc,9};1011use anyhow::{Result, bail, Context};12use clap::{ArgGroup, Parser};13use serde::de::DeserializeOwned;14use tempfile::NamedTempFile;1516use crate::{17 command::MyCommand,18 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},19};2021pub struct FleetConfigInternals {22 pub local_system: String,23 pub directory: PathBuf,24 pub opts: FleetOpts,25 pub data: RefCell<FleetData>,26 pub nix_args: Vec<OsString>,27}2829#[derive(Clone)]30pub struct Config(Arc<FleetConfigInternals>);3132impl Deref for Config {33 type Target = FleetConfigInternals;3435 fn deref(&self) -> &Self::Target {36 &self.037 }38}3940impl Config {41 pub fn should_skip(&self, host: &str) -> bool {42 if !self.opts.skip.is_empty() {43 self.opts.skip.iter().any(|h| h as &str == host)44 } else if !self.opts.only.is_empty() {45 !self.opts.only.iter().any(|h| h as &str == host)46 } else {47 false48 }49 }50 pub fn is_local(&self, host: &str) -> bool {51 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)52 }5354 pub async fn run_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<()> {55 if sudo {56 command = command.sudo();57 }58 if !self.is_local(host) {59 command = command.ssh(host);60 }61 command.run().await62 }63 #[must_use]64 pub async fn run_string_on(&self, host: &str, mut command: MyCommand, sudo: bool) -> Result<String> {65 if sudo {66 command = command.sudo();67 }68 if !self.is_local(host) {69 command = command.ssh(host);70 }71 command.run_string().await72 }7374 pub fn configuration_attr_name(&self, name: &str) -> OsString {75 let mut str = self.directory.as_os_str().to_owned();76 str.push("#");77 str.push(&format!(78 "fleetConfigurations.default.{}.{}",79 self.local_system, name80 ));81 str82 }8384 pub async fn list_hosts(&self) -> Result<Vec<String>> {85 let mut cmd = MyCommand::new("nix");86 cmd.arg("eval")87 .arg(self.configuration_attr_name("configuredHosts"))88 .args(["--apply", "builtins.attrNames", "--json", "--show-trace"])89 .args(&self.nix_args);90 cmd.run_nix_json()91 .await92 }93 pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {94 let mut cmd = MyCommand::new("nix");95 cmd.arg("eval")96 .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))97 .args(["--json", "--show-trace"])98 .args(&self.nix_args);99 cmd.run_nix_json()100 .await101 }102 pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {103 let mut cmd = MyCommand::new("nix");104 cmd.arg("eval")105 .arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))106 .args(["--apply", "builtins.attrNames"])107 .args(["--json", "--show-trace"])108 .args(&self.nix_args);109 cmd.run_nix_json()110 .await111 }112 pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {113 let mut cmd = MyCommand::new("nix");114 cmd.arg("eval")115 .arg(116 self.configuration_attr_name(&format!(117 "configuredSystems.{}.config.{}",118 host, attr119 )),120 )121 .args(["--json", "--show-trace"])122 .args(&self.nix_args);123 cmd.run_nix_json()124 .await125 }126127 pub(super) fn data(&self) -> Ref<FleetData> {128 self.data.borrow()129 }130 pub(super) fn data_mut(&self) -> RefMut<FleetData> {131 self.data.borrow_mut()132 }133134 pub fn list_shared(&self) -> Vec<String> {135 let data = self.data();136 data.shared_secrets.keys().cloned().collect()137 }138 pub fn has_shared(&self, name: &str) -> bool {139 let data = self.data();140 data.shared_secrets.contains_key(name)141 }142 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {143 let mut data = self.data_mut();144 data.shared_secrets.insert(name.to_owned(), shared);145 }146 pub fn remove_shared(&self, secret: &str) {147 let mut data = self.data_mut();148 data.shared_secrets.remove(secret);149 }150151 pub fn list_secrets(&self, host: &str) -> Vec<String> {152 let data = self.data();153 let Some(host_secrets) = data.host_secrets.get(host) else {154 return Vec::new(); 155 };156 host_secrets.keys().cloned().collect()157 }158 pub fn has_secret(&self, host: &str, secret: &str) -> bool {159 let data = self.data();160 let Some(host_secrets) = data.host_secrets.get(host) else {161 return false; 162 };163 host_secrets.contains_key(secret)164 }165 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {166 let mut data = self.data_mut();167 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();168 host_secrets.insert(secret, value);169 }170171 pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>>{172 let data = z85::encode(&data);173 let mut cmd = MyCommand::new("fleet-install-secrets");174 cmd.arg("decrypt").eqarg("--secret", data);175 cmd = cmd.sudo().ssh(host);176 let encoded = cmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();177 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)178 }179 pub async fn reencrypt_on_host(&self, host: &str, data: Vec<u8>, targets: Vec<String>) -> Result<Vec<u8>>{180 let data = z85::encode(&data);181 let mut recmd = MyCommand::new("fleet-install-secrets");182 recmd.arg("reencrypt").eqarg("--secret",data);183 for target in targets {184 recmd.eqarg("--targets", target);185 }186 recmd = recmd.sudo().ssh(host);187 let encoded = recmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();188 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)189 }190191 #[must_use]192 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {193 let data = self.data();194 let Some(host_secrets) = data.host_secrets.get(host) else {195 bail!("no secrets for machine {host}");196 };197 let Some(secret) = host_secrets.get(secret) else {198 bail!("machine {host} has no secret {secret}");199 };200 Ok(secret.clone())201 }202 #[must_use]203 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {204 let data = self.data();205 let Some(secret) = data.shared_secrets.get(secret) else {206 bail!("no shared secret {secret}");207 };208 Ok(secret.clone())209 }210211 pub fn save(&self) -> Result<()> {212 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;213 let data = nixlike::serialize(&self.data() as &FleetData)?;214 tempfile.write_all(215 format!(216 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",217 data218 )219 .as_bytes(),220 )?;221 let mut fleet_data_path = self.directory.clone();222 fleet_data_path.push("fleet.nix");223 tempfile.persist(fleet_data_path)?;224 Ok(())225 }226}227228#[derive(Parser, Clone)]229#[clap(group = ArgGroup::new("target_hosts"))]230pub struct FleetOpts {231 /// All hosts except those would be skipped232 #[clap(long, number_of_values = 1, group = "target_hosts")]233 only: Vec<String>,234235 /// Hosts to skip236 #[clap(long, number_of_values = 1, group = "target_hosts")]237 skip: Vec<String>,238239 /// Host, which should be threaten as current machine240 #[clap(long)]241 pub localhost: Option<String>,242243 // TODO: unhardcode x86_64-linux244 /// Override detected system for host, to perform builds via245 /// binfmt-declared qemu instead of trying to crosscompile246 #[clap(long, default_value = "x86_64-linux")]247 pub local_system: String,248}249250impl FleetOpts {251 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {252 let local_system = self.local_system.clone();253 if self.localhost.is_none() {254 self.localhost255 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());256 }257 let directory = current_dir()?;258259 let mut fleet_data_path = directory.clone();260 fleet_data_path.push("fleet.nix");261 let bytes = std::fs::read_to_string(fleet_data_path)?;262 let data = nixlike::parse_str(&bytes)?;263264 Ok(Config(Arc::new(FleetConfigInternals {265 opts: self,266 directory,267 data,268 local_system,269 nix_args,270 })))271 }272}cmds/fleet/src/keys.rsdiffbeforeafterboth--- 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)
}
cmds/install-secrets/src/main.rsdiffbeforeafterboth--- 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));
}
modules/fleet/secrets.nixdiffbeforeafterboth--- 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;
};
};
};
nixos/secrets.nixdiffbeforeafterboth--- 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 "<missingno>");
+ 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 "<missingno>");
+ 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
{