difftreelog
build update to nixos release-25.05
in: trunk
34 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "abort-on-drop"
@@ -377,29 +377,6 @@
]
[[package]]
-name = "bindgen"
-version = "0.69.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
-dependencies = [
- "bitflags",
- "cexpr",
- "clang-sys",
- "itertools 0.12.1",
- "lazy_static",
- "lazycell",
- "log",
- "prettyplease",
- "proc-macro2",
- "quote",
- "regex",
- "rustc-hash",
- "shlex",
- "syn",
- "which",
-]
-
-[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -488,15 +465,6 @@
]
[[package]]
-name = "cexpr"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
-dependencies = [
- "nom 7.1.3",
-]
-
-[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -556,17 +524,6 @@
"crypto-common",
"inout",
"zeroize",
-]
-
-[[package]]
-name = "clang-sys"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
-dependencies = [
- "glob",
- "libc",
- "libloading",
]
[[package]]
@@ -1242,12 +1199,6 @@
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-
-[[package]]
-name = "glob"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "h2"
@@ -1332,15 +1283,6 @@
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
-]
-
-[[package]]
-name = "home"
-version = "0.5.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
-dependencies = [
- "windows-sys 0.52.0",
]
[[package]]
@@ -1648,15 +1590,6 @@
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
-
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
[[package]]
name = "itertools"
@@ -1701,28 +1634,12 @@
]
[[package]]
-name = "lazycell"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
-
-[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
-name = "libloading"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
-dependencies = [
- "cfg-if",
- "windows-targets",
-]
-
-[[package]]
name = "libm"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1887,14 +1804,6 @@
"tokio-util",
"tracing",
"unindent",
-]
-
-[[package]]
-name = "nix-native-eval"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "nixrs",
]
[[package]]
@@ -1912,25 +1821,6 @@
]
[[package]]
-name = "nixrs"
-version = "0.1.0"
-source = "git+https://github.com/Anillc/nixrs#740fcf4048cc5b6de8c54d18254f12d53909a867"
-dependencies = [
- "libc",
- "nixrs-sys",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "nixrs-sys"
-version = "0.1.0"
-source = "git+https://github.com/Anillc/nixrs#740fcf4048cc5b6de8c54d18254f12d53909a867"
-dependencies = [
- "bindgen",
- "pkg-config",
-]
-
-[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2224,12 +2114,6 @@
"der",
"spki",
]
-
-[[package]]
-name = "pkg-config"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "poly1305"
@@ -3752,18 +3636,6 @@
dependencies = [
"js-sys",
"wasm-bindgen",
-]
-
-[[package]]
-name = "which"
-version = "4.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
-dependencies = [
- "either",
- "home",
- "once_cell",
- "rustix 0.38.40",
]
[[package]]
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,8 +2,8 @@
members = ["crates/*", "cmds/*"]
resolver = "2"
package.version = "0.1.0"
-package.edition = "2021"
-package.rust-version = "1.82.0"
+package.edition = "2024"
+package.rust-version = "1.86.0"
[workspace.dependencies]
better-command = { path = "./crates/better-command" }
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -1,15 +1,15 @@
use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
use clap::Parser;
use fleet_base::{
- deploy::{deploy_task, upload_task, DeployAction},
+ deploy::{DeployAction, deploy_task, upload_task},
host::{Config, DeployKind, GenerationStorage},
opts::FleetOpts,
};
-use nix_eval::{nix_go, NixBuildBatch};
+use nix_eval::{NixBuildBatch, nix_go};
use tokio::task::LocalSet;
-use tracing::{error, field, info, info_span, warn, Instrument};
+use tracing::{Instrument, error, field, info, info_span, warn};
#[derive(Parser)]
pub struct Deploy {
@@ -167,11 +167,12 @@
self.action,
&host,
remote_path,
- if let Ok(v) = opts.action_attr(&host, "specialisation").await {
- v
- } else {
- error!("unreachable? failed to get specialization");
- return;
+ match opts.action_attr(&host, "specialisation").await {
+ Ok(v) => v,
+ _ => {
+ error!("unreachable? failed to get specialization");
+ return;
+ }
},
disable_rollback,
)
cmds/fleet/src/cmds/info.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -1,6 +1,6 @@
use std::collections::BTreeSet;
-use anyhow::{ensure, Result};
+use anyhow::{Result, ensure};
use clap::Parser;
use fleet_base::host::Config;
use nix_eval::nix_go_json;
cmds/fleet/src/cmds/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/mod.rs
+++ b/cmds/fleet/src/cmds/mod.rs
@@ -1,6 +1,6 @@
pub mod build_systems;
pub mod complete;
pub mod info;
+pub mod rollback;
pub mod secrets;
pub mod tf;
-pub mod rollback;
\ No newline at end of file
cmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/rollback.rs
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -1,9 +1,9 @@
use std::collections::HashSet;
-use anyhow::{bail, Result};
+use anyhow::{Result, bail};
use clap::Parser;
use fleet_base::{
- deploy::{deploy_task, upload_task, DeployAction},
+ deploy::{DeployAction, deploy_task, upload_task},
host::{Config, ConfigHost, Generation, GenerationStorage},
opts::FleetOpts,
};
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -1,25 +1,25 @@
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
- io::{self, stdin, stdout, Read, Write},
+ io::{self, Read, Write, stdin, stdout},
path::PathBuf,
};
use age::Recipient;
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
use chrono::{DateTime, Utc};
use clap::Parser;
use fleet_base::{
- fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},
+ fleetdata::{FleetSecret, FleetSecretPart, FleetSharedSecret, encrypt_secret_data},
host::Config,
opts::FleetOpts,
};
use fleet_shared::SecretData;
-use nix_eval::{nix_go, nix_go_json, NixBuildBatch, Value};
+use nix_eval::{NixBuildBatch, Value, nix_go, nix_go_json};
use owo_colors::OwoColorize;
use serde::Deserialize;
use tabled::{Table, Tabled};
use tokio::fs::read;
-use tracing::{error, info, info_span, warn, Instrument};
+use tracing::{Instrument, error, info, info_span, warn};
#[derive(Parser)]
pub enum Secret {
@@ -187,7 +187,9 @@
true
} else if set.difference(&expected_set).next().is_some() {
// TODO: Remove this warning for revokable secrets.
- warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");
+ warn!(
+ "host was removed from secret owners, but until this host rebuild, the secret will still be stored on it."
+ );
nix_go_json!(field.regenerateOnOwnerRemoved)
} else if expected_set.difference(&set).next().is_some() {
nix_go_json!(field.regenerateOnOwnerAdded)
@@ -296,8 +298,8 @@
let out_parent = host.mktemp_dir().await?;
let out = format!("{out_parent}/out");
- let mut gen = host.cmd(generator).await?;
- gen.env("out", &out);
+ let mut r#gen = host.cmd(generator).await?;
+ r#gen.env("out", &out);
if on.is_none() {
// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.
let project_path: String = config
@@ -306,9 +308,9 @@
.into_os_string()
.into_string()
.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;
- gen.env("FLEET_PROJECT", project_path);
+ r#gen.env("FLEET_PROJECT", project_path);
}
- gen.run().await.context("impure generator")?;
+ r#gen.run().await.context("impure generator")?;
{
let marker = host.read_file_text(format!("{out}/marker")).await?;
@@ -510,7 +512,9 @@
if !remove_machines.is_empty() {
// TODO: maybe force secret regeneration?
// Not that useful without revokation.
- warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");
+ warn!(
+ "secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"
+ );
}
Ok(target_machines)
}
@@ -596,7 +600,9 @@
part: part_name,
} => {
if config.has_secret(&machine, &name) && !replace && !merge {
- bail!("secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret");
+ bail!(
+ "secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret"
+ );
}
let mut out = if merge && !replace {
cmds/fleet/src/extra_args.rsdiffbeforeafterboth--- a/cmds/fleet/src/extra_args.rs
+++ b/cmds/fleet/src/extra_args.rs
@@ -1,6 +1,6 @@
use std::ffi::{OsStr, OsString};
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
pub fn parse_os(os: &OsStr) -> Result<Vec<OsString>> {
Ok(shlex::bytes::split(os.as_encoded_bytes())
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -6,27 +6,27 @@
use std::{ffi::OsString, process::ExitCode};
-use anyhow::{bail, Result};
+use anyhow::{Result, bail};
use clap::{CommandFactory, Parser};
use cmds::{
build_systems::{BuildSystems, Deploy},
- rollback::RollbackSingle,
complete::Complete,
info::Info,
+ rollback::RollbackSingle,
secrets::Secret,
tf::Tf,
};
use fleet_base::{host::Config, opts::FleetOpts};
-use futures::{future::LocalBoxFuture, stream::FuturesUnordered, TryStreamExt};
+use futures::{TryStreamExt, future::LocalBoxFuture, stream::FuturesUnordered};
// use host::Config;
#[cfg(feature = "indicatif")]
use human_repr::HumanCount;
#[cfg(feature = "indicatif")]
use indicatif::{ProgressState, ProgressStyle};
-use tracing::{error, info, info_span, Instrument};
+use tracing::{Instrument, error, info, info_span};
#[cfg(feature = "indicatif")]
use tracing_indicatif::IndicatifLayer;
-use tracing_subscriber::{prelude::*, EnvFilter};
+use tracing_subscriber::{EnvFilter, prelude::*};
#[derive(Parser)]
struct Prefetch {}
cmds/generator-helper/src/main.rsdiffbeforeafterboth--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -1,21 +1,22 @@
use std::{
env,
fs::{File, OpenOptions},
- io::{self, copy, stdin, stdout, Read, Write},
+ io::{self, Read, Write, copy, stdin, stdout},
str::FromStr,
};
use age::{
+ Encryptor, Recipient,
ssh::{ParseRecipientKeyError, Recipient as SshRecipient},
- Encryptor, Recipient,
};
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
use clap::{Parser, ValueEnum};
use ed25519_dalek::SecretKey;
use fleet_shared::SecretData;
use rand::{
+ RngCore,
distr::{Alphanumeric, Distribution, SampleString, Uniform},
- rng, RngCore,
+ rng,
};
fn write_output_file(out: &str) -> Result<File> {
@@ -78,7 +79,9 @@
let list = match list {
Ok(v) => v,
Err(env::VarError::NotPresent) => {
- bail!("gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities");
+ bail!(
+ "gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities"
+ );
}
Err(e) => bail!("somehow, identities list is not utf-8: {e}"),
};
@@ -254,13 +257,7 @@
write_private(
&recipients,
&private,
- &key[..{
- if no_embed_public {
- 32
- } else {
- 64
- }
- }],
+ &key[..{ if no_embed_public { 32 } else { 64 } }],
encoding,
)?;
}
cmds/install-secrets/src/main.rsdiffbeforeafterboth--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -5,20 +5,20 @@
iter,
os::unix::prelude::PermissionsExt,
path::{Path, PathBuf},
- str::{from_utf8, FromStr},
+ str::{FromStr, from_utf8},
};
use age::{
- ssh::{Identity as SshIdentity, Recipient as SshRecipient},
Decryptor, Encryptor, Identity, Recipient,
+ ssh::{Identity as SshIdentity, Recipient as SshRecipient},
};
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
use clap::Parser;
use fleet_shared::SecretData;
-use nix::unistd::{chown, Group, User};
+use nix::unistd::{Group, User, chown};
use serde::Deserialize;
use tracing::{error, info, info_span};
-use tracing_subscriber::{filter::LevelFilter, EnvFilter};
+use tracing_subscriber::{EnvFilter, filter::LevelFilter};
#[derive(Parser)]
#[clap(author)]
cmds/terraform-provider-fleet/src/main.rsdiffbeforeafterboth--- a/cmds/terraform-provider-fleet/src/main.rs
+++ b/cmds/terraform-provider-fleet/src/main.rs
@@ -1,3 +1,5 @@
fn main() {
- panic!("this is a stub, real provider is in development, I just don't want to keep it in different branch.")
+ panic!(
+ "this is a stub, real provider is in development, I just don't want to keep it in different branch."
+ )
}
crates/better-command/src/handler.rsdiffbeforeafterboth--- a/crates/better-command/src/handler.rs
+++ b/crates/better-command/src/handler.rs
@@ -7,7 +7,7 @@
use regex::Regex;
use serde::Deserialize;
-use tracing::{info, info_span, warn, Span};
+use tracing::{Span, info, info_span, warn};
#[cfg(feature = "indicatif")]
use tracing_indicatif::span_ext::IndicatifSpanExt as _;
@@ -112,9 +112,13 @@
match log {
NixLog::Msg { msg, raw_msg, .. } => {
#[allow(clippy::nonminimal_bool)]
- 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 !(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 {
if !msg.is_empty() {
info!(target: "nix", "{}\n{}", raw_msg.trim_end(), msg.trim_end())
@@ -156,8 +160,12 @@
id,
..
} if typ == 100 && fields.len() >= 3 => {
- if let [LogField::String(drv), LogField::String(from), LogField::String(to), ..] =
- &fields[..]
+ if let [
+ LogField::String(drv),
+ LogField::String(from),
+ LogField::String(to),
+ ..,
+ ] = &fields[..]
{
let mut drv = drv.as_str();
@@ -289,8 +297,12 @@
}
NixLog::Result { fields, id, typ } if typ == 105 && fields.len() >= 4 => {
if let Some(span) = self.spans.get(&id) {
- if let [LogField::Num(done), LogField::Num(expected), LogField::Num(_running), LogField::Num(_failed)] =
- &fields[..4]
+ if let [
+ LogField::Num(done),
+ LogField::Num(expected),
+ LogField::Num(_running),
+ LogField::Num(_failed),
+ ] = &fields[..4]
{
#[cfg(feature = "indicatif")]
{
crates/fleet-base/src/command.rsdiffbeforeafterboth--- a/crates/fleet-base/src/command.rs
+++ b/crates/fleet-base/src/command.rs
@@ -1,6 +1,6 @@
use std::{ffi::OsStr, pin, process::Stdio, sync::Arc, task::Poll};
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
use better_command::{Handler, NixHandler, PlainHandler};
use futures::StreamExt;
use itertools::Either;
@@ -68,10 +68,9 @@
}
}
fn new_here(&self, cmd: impl AsRef<OsStr>) -> Self {
- if let Some(ssh_session) = self.ssh_session.clone() {
- Self::new_on(self.escalation, cmd, ssh_session)
- } else {
- Self::new(self.escalation, cmd)
+ match self.ssh_session.clone() {
+ Some(ssh_session) => Self::new_on(self.escalation, cmd, ssh_session),
+ _ => Self::new(self.escalation, cmd),
}
}
@@ -139,15 +138,18 @@
out
}
fn into_command(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {
- Ok(if let Some(session) = self.ssh_session.clone() {
- let cmd = self.translate_env_into_env().into_command_unchecked_local();
- Either::Right(
- cmd.over_ssh(session)
- .map_err(|e| anyhow!("ssh error: {e}"))?,
- )
- } else {
- let cmd = self.into_command_unchecked_local();
- Either::Left(cmd)
+ Ok(match self.ssh_session.clone() {
+ Some(session) => {
+ let cmd = self.translate_env_into_env().into_command_unchecked_local();
+ Either::Right(
+ cmd.over_ssh(session)
+ .map_err(|e| anyhow!("ssh error: {e}"))?,
+ )
+ }
+ _ => {
+ let cmd = self.into_command_unchecked_local();
+ Either::Left(cmd)
+ }
})
}
pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
crates/fleet-base/src/deploy.rsdiffbeforeafterboth--- a/crates/fleet-base/src/deploy.rs
+++ b/crates/fleet-base/src/deploy.rs
@@ -1,10 +1,10 @@
use std::{path::PathBuf, time::Duration};
-use anyhow::{anyhow, bail, Context as _, Result};
+use anyhow::{Context as _, Result, anyhow, bail};
use clap::ValueEnum;
use itertools::Itertools;
use tokio::time::sleep;
-use tracing::{error, info, info_span, warn, Instrument as _};
+use tracing::{Instrument as _, error, info, info_span, warn};
use crate::host::{Config, ConfigHost, DeployKind, Generation, GenerationStorage};
@@ -221,7 +221,9 @@
.in_current_span()
.await
{
- error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")
+ error!(
+ "failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}"
+ )
}
}
info!("disarming watchdog, just in case");
@@ -233,12 +235,17 @@
error!("failed to disarm rollback run: {e}");
}
}
- } else if let Err(_e) = host
- .rm_file("/etc/fleet_rollback_marker", true)
- .in_current_span()
- .await
- {
- // Marker might not exist, yet better try to remove it.
+ } else {
+ match host
+ .rm_file("/etc/fleet_rollback_marker", true)
+ .in_current_span()
+ .await
+ {
+ Err(_e) => {
+ // Marker might not exist, yet better try to remove it.
+ }
+ _ => {}
+ }
}
}
Ok(())
crates/fleet-base/src/fleetdata.rsdiffbeforeafterboth--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -10,7 +10,7 @@
distr::{Alphanumeric, SampleString as _},
rng,
};
-use serde::{de::Error, Deserialize, Serialize};
+use serde::{Deserialize, Serialize, de::Error};
use serde_json::Value;
#[derive(Serialize, Deserialize, Default)]
crates/fleet-base/src/host.rsdiffbeforeafterboth1use std::{2 cell::OnceCell,3 collections::BTreeSet,4 ffi::{OsStr, OsString},5 fmt::Display,6 io::Write,7 ops::Deref,8 path::PathBuf,9 str::FromStr,10 sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{anyhow, bail, ensure, Context, Result};14use fleet_shared::SecretData;15use nix_eval::{nix_go, nix_go_json, util::assert_warn, NixSession, Value};16use openssh::SessionBuilder;17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{format_description, UtcDateTime};21use tracing::warn;2223use crate::{24 command::MyCommand,25 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},26};2728pub struct FleetConfigInternals {29 /// Fleet project directory, containing fleet.nix file.30 pub directory: PathBuf,31 /// builtins.currentSystem32 pub local_system: String,33 pub data: Mutex<FleetData>,34 pub nix_args: Vec<OsString>,35 /// fleet_config.config36 pub config_field: Value,37 // TODO: Remove with connectivity refactor38 pub localhost: String,3940 /// import nixpkgs {system = local};41 pub default_pkgs: Value,42 /// inputs.nixpkgs43 pub nixpkgs: Value,4445 pub nix_session: NixSession,46}4748// TODO: Make field not pub49#[derive(Clone)]50pub struct Config(pub Arc<FleetConfigInternals>);5152impl Deref for Config {53 type Target = FleetConfigInternals;5455 fn deref(&self) -> &Self::Target {56 &self.057 }58}5960#[derive(Clone, Copy, Debug)]61pub enum EscalationStrategy {62 Sudo,63 Run0,64 Su,65}6667#[derive(Clone, PartialEq, Copy, Debug)]68pub enum DeployKind {69 /// NixOS => NixOS managed by fleet70 UpgradeToFleet,71 /// NixOS managed by fleet => NixOS managed by fleet72 Fleet,73 /// Remote host has /mnt, /mnt/boot mounted,74 /// generated config is added to fleet configuration.75 NixosInstall,76 /// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),77 /// generated config is added to fleet configuration,78 /// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.79 NixosLustrate,80}8182impl FromStr for DeployKind {83 type Err = anyhow::Error;84 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {85 match s {86 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),87 "fleet" => Ok(Self::Fleet),88 "nixos-install" => Ok(Self::NixosInstall),89 "nixos-lustrate" => Ok(Self::NixosLustrate),90 v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""),91 }92 }93}94pub struct ConfigHost {95 config: Config,96 pub name: String,97 groups: OnceCell<Vec<String>>,9899 deploy_kind: OnceCell<DeployKind>,100101 pub host_config: Option<Value>,102 pub nixos_config: OnceCell<Value>,103 pub nixos_unchecked_config: OnceCell<Value>,104 pub pkgs_override: Option<Value>,105106 // TODO: Move command helpers away with connectivity refactor107 pub local: bool,108 pub session: OnceLock<Arc<openssh::Session>>,109}110111#[derive(Debug, Clone, Copy)]112pub enum GenerationStorage {113 Deployer,114 Machine,115 Pusher,116}117impl GenerationStorage {118 fn prefix(&self) -> &'static str {119 match self {120 GenerationStorage::Deployer => "deployer.",121 GenerationStorage::Machine => "",122 GenerationStorage::Pusher => "pusher.",123 }124 }125}126127#[derive(Tabled, Debug)]128pub struct Generation {129 #[tabled(rename = "ID", format("{}", self.rollback_id()))]130 pub id: u32,131 #[tabled(rename = "Current")]132 pub current: bool,133 #[tabled(rename = "Created at")]134 pub datetime: UtcDateTime,135 #[tabled(format = "{:?}")]136 pub store_path: PathBuf,137 #[tabled(skip)]138 pub location: GenerationStorage,139}140impl Generation {141 pub fn rollback_id(&self) -> String {142 format!("{}{}", self.location.prefix(), self.id)143 }144}145146fn parse_generation_line(g: &str) -> Option<Generation> {147 let mut parts = g.split_whitespace();148 let id = parts.next()?;149 let id: u32 = id.parse().ok()?;150 let date = parts.next()?;151 let time = parts.next()?;152 let current = if let Some(current) = parts.next() {153 if current == "(current)" {154 Some(true)155 } else {156 None157 }158 } else {159 Some(false)160 };161 let current = current?;162 if parts.next().is_some() {163 warn!("unexpected text after generation: {g}");164 }165166 let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")167 .expect("valid format");168 let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;169170 Some(Generation {171 id,172 current,173 datetime,174 store_path: PathBuf::new(),175 location: GenerationStorage::Machine,176 })177}178// TODO: Move command helpers away with connectivity refactor179impl ConfigHost {180 pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {181 let mut cmd = self.cmd("nix-env").await?;182 cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))183 .arg("--list-generations")184 .env("TZ", "UTC");185 // Sudo is required because --list-generations tries to acquire profile lock186 let data = cmd.sudo().run_string().await?;187 let mut generations = data188 .split('\n')189 .map(|e| e.trim())190 .filter(|&l| !l.is_empty())191 .filter_map(|g| {192 let gen = parse_generation_line(g);193 if gen.is_none() {194 warn!("bad generation: {g}");195 };196 gen197 })198 .collect::<Vec<_>>();199 for ele in generations.iter_mut() {200 let mut cmd = self.cmd("readlink").await?;201 cmd.arg("--")202 .arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));203 let path = cmd.run_string().await?;204 ele.store_path = PathBuf::from(path.trim_end_matches("\n"));205 }206207 Ok(generations)208 }209210 pub fn set_deploy_kind(&self, kind: DeployKind) {211 self.deploy_kind212 .set(kind)213 .ok()214 .expect("deploy kind is already set");215 }216 pub async fn deploy_kind(&self) -> Result<DeployKind> {217 if let Some(kind) = self.deploy_kind.get() {218 return Ok(kind.clone());219 }220 let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {221 Ok(v) => v,222 Err(e) => {223 bail!("failed to query remote system kind: {}", e);224 }225 };226 if !is_fleet_managed {227 bail!(indoc::indoc! {"228 host is not marked as managed by fleet229 if you're not trying to lustrate/install system from scratch,230 you should either231 1. manually create /etc/FLEET_HOST file on the target host,232 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet233 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos234 "});235 }236 // TOCTOU is possible237 let _ = self.deploy_kind.set(DeployKind::Fleet);238 Ok(self239 .deploy_kind240 .get()241 .expect("deploy kind is just set")242 .clone())243 }244 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {245 // Prefer sudo, as run0 has some gotchas with polkit246 // and too many repeating prompts.247 if (self.find_in_path("sudo").await).is_ok() {248 return Ok(EscalationStrategy::Sudo);249 }250 if (self.find_in_path("run0").await).is_ok() {251 return Ok(EscalationStrategy::Run0);252 }253 Ok(EscalationStrategy::Su)254 }255 async fn open_session(&self) -> Result<Arc<openssh::Session>> {256 assert!(!self.local, "do not open ssh connection to local session");257 // FIXME: TOCTOU258 if let Some(session) = &self.session.get() {259 return Ok((*session).clone());260 };261 let session = SessionBuilder::default();262 let session = session263 .connect(&self.name)264 .await265 .map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;266 let session = Arc::new(session);267 self.session.set(session.clone()).expect("TOCTOU happened");268 Ok(session)269 }270 pub async fn mktemp_dir(&self) -> Result<String> {271 let mut cmd = self.cmd("mktemp").await?;272 cmd.arg("-d");273 let path = cmd.run_string().await?;274 Ok(path.trim_end().to_owned())275 }276 pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {277 let mut cmd = self.cmd("sh").await?;278 cmd.arg("-c")279 .arg("test -e \"$1\" && echo true || echo false")280 .arg("_")281 .arg(path);282 Ok(cmd.run_value().await?)283 }284 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {285 let mut cmd = self.cmd("cat").await?;286 cmd.arg(path);287 cmd.run_bytes().await288 }289 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {290 let mut cmd = self.cmd("cat").await?;291 cmd.arg(path);292 cmd.run_string().await293 }294 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {295 let mut cmd = self.cmd("ls").await?;296 cmd.arg(path);297 let out = cmd.run_string().await?;298 let mut lines = out.split('\n');299 if let Some(last) = lines.next_back() {300 ensure!(last.is_empty(), "output of ls should end with newline");301 }302 Ok(lines.map(ToOwned::to_owned).collect())303 }304 #[allow(dead_code)]305 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {306 let text = self.read_file_text(path).await?;307 Ok(serde_json::from_str(&text)?)308 }309 pub async fn read_env(&self, env: &str) -> Result<String> {310 let mut cmd = self.cmd("printenv").await?;311 cmd.arg(env);312 cmd.run_string().await313 }314 pub async fn find_in_path(&self, command: &str) -> Result<String> {315 // // `which` is not a part of coreutils, and it might not exist on machine.316 // let path = self.read_env("PATH").await?;317 // // Assuming delimiter is :, we don't work with windows host, this check will be much318 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)319 // for ele in path.split(':') {320 // let test_path = format!("{ele}/{cmd}");321 // test -x etc322 // }323 // let mut cmd = self.cmd("printenv").await?;324 // cmd.arg(env);325 // Ok(cmd.run_string().await?)326 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.327 let mut cmd = self328 .cmd_escalation(329 // Not used330 EscalationStrategy::Su,331 "which",332 )333 .await?;334 cmd.arg(command);335 cmd.run_string().await336 }337 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>338 where339 <D as FromStr>::Err: Display,340 {341 let text = self.read_file_text(path).await?;342 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))343 }344 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {345 self.cmd_escalation(self.escalation_strategy().await?, cmd)346 .await347 }348 pub async fn cmd_escalation(349 &self,350 escalation: EscalationStrategy,351 cmd: impl AsRef<OsStr>,352 ) -> Result<MyCommand> {353 if self.local {354 Ok(MyCommand::new(escalation, cmd))355 } else {356 let session = self.open_session().await?;357 Ok(MyCommand::new_on(escalation, cmd, session))358 }359 }360 pub async fn nix_cmd(&self) -> Result<MyCommand> {361 let mut nix = self.cmd("nix").await?;362 nix.args([363 "--extra-experimental-features",364 "nix-command",365 "--extra-experimental-features",366 "flakes",367 ]);368 Ok(nix)369 }370371 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {372 ensure!(data.encrypted, "secret is not encrypted");373 let mut cmd = self.cmd("fleet-install-secrets").await?;374 cmd.arg("decrypt").eqarg("--secret", data.to_string());375 let encoded = cmd376 .sudo()377 .run_string()378 .await379 .context("failed to call remote host for decrypt")?;380 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;381 ensure!(!data.encrypted, "secret came out encrypted");382 Ok(data.data)383 }384 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {385 ensure!(data.encrypted, "secret is not encrypted");386 let mut cmd = self.cmd("fleet-install-secrets").await?;387 cmd.arg("reencrypt").eqarg("--secret", data.to_string());388 for target in targets {389 let key = self.config.key(&target).await?;390 cmd.eqarg("--targets", key);391 }392 let encoded = cmd393 .sudo()394 .run_string()395 .await396 .context("failed to call remote host for decrypt")?;397 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;398 ensure!(data.encrypted, "secret came out not encrypted");399 Ok(data)400 }401 /// Returns path for futureproofing, as path might change i.e on conversion to CA402 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {403 if self.local {404 // Path is located locally, thus already trusted.405 return Ok(path.to_owned());406 }407 let mut nix = MyCommand::new(408 // Not used409 EscalationStrategy::Su,410 "nix",411 );412 nix.arg("copy").arg("--substitute-on-destination");413414 match self.deploy_kind().await? {415 DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {416 nix.comparg("--to", format!("ssh-ng://{}", self.name));417 }418 DeployKind::NixosInstall => {419 nix420 // Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon421 .arg("--no-check-sigs")422 .comparg(423 "--to",424 format!("ssh-ng://root@{}?remote-store=/mnt", self.name),425 );426 }427 }428 nix.arg(path);429 nix.run_nix().await.context("nix copy")?;430 Ok(path.to_owned())431 }432 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {433 let mut cmd = self.cmd("systemctl").await?;434 cmd.arg("stop").arg(name);435 cmd.sudo().run().await436 }437 pub async fn systemctl_start(&self, name: &str) -> Result<()> {438 let mut cmd = self.cmd("systemctl").await?;439 cmd.arg("start").arg(name);440 cmd.sudo().run().await441 }442443 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {444 let mut cmd = self.cmd("rm").await?;445 cmd.arg("-f").arg(path);446 if sudo {447 cmd = cmd.sudo()448 }449 cmd.run().await450 }451}452impl ConfigHost {453 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,454 // assuming getting tags always returns the same value.455 pub async fn tags(&self) -> Result<Vec<String>> {456 if let Some(v) = self.groups.get() {457 return Ok(v.clone());458 }459 let Some(host_config) = &self.host_config else {460 return Ok(vec![]);461 };462 let tags: Vec<String> = nix_go_json!(host_config.tags);463464 let _ = self.groups.set(tags.clone());465466 Ok(tags)467 }468 pub async fn nixos_config(&self) -> Result<Value> {469 if let Some(v) = self.nixos_config.get() {470 return Ok(v.clone());471 }472 let Some(host_config) = &self.host_config else {473 bail!("local host has no nixos_config");474 };475 let nixos_config = nix_go!(host_config.nixos.config);476 assert_warn("nixos config evaluation", &nixos_config).await?;477478 let _ = self.nixos_config.set(nixos_config.clone());479480 Ok(nixos_config)481 }482 pub async fn nixos_unchecked_config(&self) -> Result<Value> {483 if let Some(v) = self.nixos_unchecked_config.get() {484 return Ok(v.clone());485 }486 let Some(host_config) = &self.host_config else {487 bail!("local host has no nixos_config");488 };489 let nixos_config = nix_go!(host_config.nixos_unchecked.config);490491 let _ = self.nixos_unchecked_config.set(nixos_config.clone());492493 Ok(nixos_config)494 }495496 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {497 let nixos = self.nixos_unchecked_config().await?;498 let secrets = nix_go!(nixos.secrets);499 let mut out = Vec::new();500 for name in secrets.list_fields().await? {501 let secret = nix_go!(secrets[{ name }]);502 let is_shared: bool = nix_go_json!(secret.shared);503 if is_shared {504 continue;505 }506 out.push(name);507 }508 Ok(out)509 }510 pub async fn secret_field(&self, name: &str) -> Result<Value> {511 let nixos = self.nixos_unchecked_config().await?;512 Ok(nix_go!(nixos.secrets[{ name }]))513 }514515 /// Packages for this host, resolved with nixpkgs overlays516 pub async fn pkgs(&self) -> Result<Value> {517 if let Some(value) = &self.pkgs_override {518 return Ok(value.clone());519 }520 let Some(host_config) = &self.host_config else {521 bail!("local host has no host_config");522 };523 // TODO: Should nixos.options be cached?524 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))525 }526}527528impl Config {529 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {530 let config = &self.config_field;531 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);532 Ok(tagged)533 }534 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {535 let mut out = BTreeSet::new();536 for owner in owners {537 if let Some(tag) = owner.strip_prefix('@') {538 let hosts = self.tagged_hostnames(tag).await?;539 out.extend(hosts);540 } else {541 out.insert(owner);542 }543 }544 Ok(out)545 }546 pub fn local_host(&self) -> ConfigHost {547 ConfigHost {548 config: self.clone(),549 name: "<virtual localhost>".to_owned(),550 host_config: None,551 nixos_config: OnceCell::new(),552 nixos_unchecked_config: OnceCell::new(),553 groups: {554 let cell = OnceCell::new();555 let _ = cell.set(vec![]);556 cell557 },558 pkgs_override: Some(self.default_pkgs.clone()),559560 local: true,561 session: OnceLock::new(),562 deploy_kind: OnceCell::new(),563 }564 }565566 pub async fn host(&self, name: &str) -> Result<ConfigHost> {567 let config = &self.config_field;568 let host_config = nix_go!(config.hosts[{ name }]);569570 Ok(ConfigHost {571 config: self.clone(),572 name: name.to_owned(),573 host_config: Some(host_config),574 nixos_config: OnceCell::new(),575 nixos_unchecked_config: OnceCell::new(),576 groups: OnceCell::new(),577 pkgs_override: None,578579 // TODO: Remove with connectivit refactor580 local: self.localhost == name,581 session: OnceLock::new(),582 deploy_kind: OnceCell::new(),583 })584 }585 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {586 let config = &self.config_field;587 let names = nix_go!(config.hosts).list_fields().await?;588 let mut out = vec![];589 for name in names {590 out.push(self.host(&name).await?);591 }592 Ok(out)593 }594 // TODO: Replace usages with .host().nixos_config595 pub async fn system_config(&self, host: &str) -> Result<Value> {596 let fleet_field = &self.config_field;597 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))598 }599600 /// Shared secrets configured in fleet.nix or in flake601 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {602 let config_field = &self.config_field;603 Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)604 }605 /// Shared secrets configured in fleet.nix606 pub fn list_shared(&self) -> Vec<String> {607 let data = self.data();608 data.shared_secrets.keys().cloned().collect()609 }610 pub fn has_shared(&self, name: &str) -> bool {611 let data = self.data();612 data.shared_secrets.contains_key(name)613 }614 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {615 let mut data = self.data_mut();616 data.shared_secrets.insert(name.to_owned(), shared);617 }618 pub fn remove_shared(&self, secret: &str) {619 let mut data = self.data_mut();620 data.shared_secrets.remove(secret);621 }622623 pub fn list_secrets(&self, host: &str) -> Vec<String> {624 let data = self.data();625 let Some(secrets) = data.host_secrets.get(host) else {626 return Vec::new();627 };628 secrets.keys().cloned().collect()629 }630631 pub fn has_secret(&self, host: &str, secret: &str) -> bool {632 let data = self.data();633 let Some(host_secrets) = data.host_secrets.get(host) else {634 return false;635 };636 host_secrets.contains_key(secret)637 }638 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {639 let mut data = self.data_mut();640 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();641 host_secrets.insert(secret, value);642 }643644 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {645 let data = self.data();646 let Some(host_secrets) = data.host_secrets.get(host) else {647 bail!("no secrets for machine {host}");648 };649 let Some(secret) = host_secrets.get(secret) else {650 bail!("machine {host} has no secret {secret}");651 };652 Ok(secret.clone())653 }654 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {655 let data = self.data();656 let Some(secret) = data.shared_secrets.get(secret) else {657 bail!("no shared secret {secret}");658 };659 Ok(secret.clone())660 }661 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {662 let config_field = &self.config_field;663 Ok(nix_go_json!(664 config_field.sharedSecrets[{ secret }].expectedOwners665 ))666 }667668 // TODO: Should this be something modifiable from other processes?669 // E.g terraform provider might want to update FleetData (e.g secrets),670 // and current implementation assumes only one process holds current fleet.nix671 // Given that it is no longer needs to be a file for nix evaluation,672 // maybe it can be a .nix file for persistence, but accessible only673 // thru some shared state controller? Might it be stored in terraform674 // state provider?675 pub fn data(&self) -> MutexGuard<FleetData> {676 self.data.lock().unwrap()677 }678 pub fn data_mut(&self) -> MutexGuard<FleetData> {679 self.data.lock().unwrap()680 }681 pub fn save(&self) -> Result<()> {682 let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;683 let data = nixlike::serialize(&self.data() as &FleetData)?;684 tempfile.write_all(685 format!(686 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",687 data688 )689 .as_bytes(),690 )?;691 let mut fleet_data_path = self.directory.clone();692 fleet_data_path.push("fleet.nix");693 tempfile.persist(fleet_data_path)?;694 Ok(())695 }696}1use std::{2 cell::OnceCell,3 collections::BTreeSet,4 ffi::{OsStr, OsString},5 fmt::Display,6 io::Write,7 ops::Deref,8 path::PathBuf,9 str::FromStr,10 sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{NixSession, Value, nix_go, nix_go_json, util::assert_warn};16use openssh::SessionBuilder;17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24 command::MyCommand,25 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},26};2728pub struct FleetConfigInternals {29 /// Fleet project directory, containing fleet.nix file.30 pub directory: PathBuf,31 /// builtins.currentSystem32 pub local_system: String,33 pub data: Mutex<FleetData>,34 pub nix_args: Vec<OsString>,35 /// fleet_config.config36 pub config_field: Value,37 // TODO: Remove with connectivity refactor38 pub localhost: String,3940 /// import nixpkgs {system = local};41 pub default_pkgs: Value,42 /// inputs.nixpkgs43 pub nixpkgs: Value,4445 pub nix_session: NixSession,46}4748// TODO: Make field not pub49#[derive(Clone)]50pub struct Config(pub Arc<FleetConfigInternals>);5152impl Deref for Config {53 type Target = FleetConfigInternals;5455 fn deref(&self) -> &Self::Target {56 &self.057 }58}5960#[derive(Clone, Copy, Debug)]61pub enum EscalationStrategy {62 Sudo,63 Run0,64 Su,65}6667#[derive(Clone, PartialEq, Copy, Debug)]68pub enum DeployKind {69 /// NixOS => NixOS managed by fleet70 UpgradeToFleet,71 /// NixOS managed by fleet => NixOS managed by fleet72 Fleet,73 /// Remote host has /mnt, /mnt/boot mounted,74 /// generated config is added to fleet configuration.75 NixosInstall,76 /// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),77 /// generated config is added to fleet configuration,78 /// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.79 NixosLustrate,80}8182impl FromStr for DeployKind {83 type Err = anyhow::Error;84 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {85 match s {86 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),87 "fleet" => Ok(Self::Fleet),88 "nixos-install" => Ok(Self::NixosInstall),89 "nixos-lustrate" => Ok(Self::NixosLustrate),90 v => bail!(91 "unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""92 ),93 }94 }95}96pub struct ConfigHost {97 config: Config,98 pub name: String,99 groups: OnceCell<Vec<String>>,100101 deploy_kind: OnceCell<DeployKind>,102103 pub host_config: Option<Value>,104 pub nixos_config: OnceCell<Value>,105 pub nixos_unchecked_config: OnceCell<Value>,106 pub pkgs_override: Option<Value>,107108 // TODO: Move command helpers away with connectivity refactor109 pub local: bool,110 pub session: OnceLock<Arc<openssh::Session>>,111}112113#[derive(Debug, Clone, Copy)]114pub enum GenerationStorage {115 Deployer,116 Machine,117 Pusher,118}119impl GenerationStorage {120 fn prefix(&self) -> &'static str {121 match self {122 GenerationStorage::Deployer => "deployer.",123 GenerationStorage::Machine => "",124 GenerationStorage::Pusher => "pusher.",125 }126 }127}128129#[derive(Tabled, Debug)]130pub struct Generation {131 #[tabled(rename = "ID", format("{}", self.rollback_id()))]132 pub id: u32,133 #[tabled(rename = "Current")]134 pub current: bool,135 #[tabled(rename = "Created at")]136 pub datetime: UtcDateTime,137 #[tabled(format = "{:?}")]138 pub store_path: PathBuf,139 #[tabled(skip)]140 pub location: GenerationStorage,141}142impl Generation {143 pub fn rollback_id(&self) -> String {144 format!("{}{}", self.location.prefix(), self.id)145 }146}147148fn parse_generation_line(g: &str) -> Option<Generation> {149 let mut parts = g.split_whitespace();150 let id = parts.next()?;151 let id: u32 = id.parse().ok()?;152 let date = parts.next()?;153 let time = parts.next()?;154 let current = if let Some(current) = parts.next() {155 if current == "(current)" {156 Some(true)157 } else {158 None159 }160 } else {161 Some(false)162 };163 let current = current?;164 if parts.next().is_some() {165 warn!("unexpected text after generation: {g}");166 }167168 let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")169 .expect("valid format");170 let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;171172 Some(Generation {173 id,174 current,175 datetime,176 store_path: PathBuf::new(),177 location: GenerationStorage::Machine,178 })179}180// TODO: Move command helpers away with connectivity refactor181impl ConfigHost {182 pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {183 let mut cmd = self.cmd("nix-env").await?;184 cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))185 .arg("--list-generations")186 .env("TZ", "UTC");187 // Sudo is required because --list-generations tries to acquire profile lock188 let data = cmd.sudo().run_string().await?;189 let mut generations = data190 .split('\n')191 .map(|e| e.trim())192 .filter(|&l| !l.is_empty())193 .filter_map(|g| {194 let generation = parse_generation_line(g);195 if generation.is_none() {196 warn!("bad generation: {g}");197 };198 generation199 })200 .collect::<Vec<_>>();201 for ele in generations.iter_mut() {202 let mut cmd = self.cmd("readlink").await?;203 cmd.arg("--")204 .arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));205 let path = cmd.run_string().await?;206 ele.store_path = PathBuf::from(path.trim_end_matches("\n"));207 }208209 Ok(generations)210 }211212 pub fn set_deploy_kind(&self, kind: DeployKind) {213 self.deploy_kind214 .set(kind)215 .ok()216 .expect("deploy kind is already set");217 }218 pub async fn deploy_kind(&self) -> Result<DeployKind> {219 if let Some(kind) = self.deploy_kind.get() {220 return Ok(kind.clone());221 }222 let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {223 Ok(v) => v,224 Err(e) => {225 bail!("failed to query remote system kind: {}", e);226 }227 };228 if !is_fleet_managed {229 bail!(indoc::indoc! {"230 host is not marked as managed by fleet231 if you're not trying to lustrate/install system from scratch,232 you should either233 1. manually create /etc/FLEET_HOST file on the target host,234 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet235 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos236 "});237 }238 // TOCTOU is possible239 let _ = self.deploy_kind.set(DeployKind::Fleet);240 Ok(self241 .deploy_kind242 .get()243 .expect("deploy kind is just set")244 .clone())245 }246 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {247 // Prefer sudo, as run0 has some gotchas with polkit248 // and too many repeating prompts.249 if (self.find_in_path("sudo").await).is_ok() {250 return Ok(EscalationStrategy::Sudo);251 }252 if (self.find_in_path("run0").await).is_ok() {253 return Ok(EscalationStrategy::Run0);254 }255 Ok(EscalationStrategy::Su)256 }257 async fn open_session(&self) -> Result<Arc<openssh::Session>> {258 assert!(!self.local, "do not open ssh connection to local session");259 // FIXME: TOCTOU260 if let Some(session) = &self.session.get() {261 return Ok((*session).clone());262 };263 let session = SessionBuilder::default();264 let session = session265 .connect(&self.name)266 .await267 .map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;268 let session = Arc::new(session);269 self.session.set(session.clone()).expect("TOCTOU happened");270 Ok(session)271 }272 pub async fn mktemp_dir(&self) -> Result<String> {273 let mut cmd = self.cmd("mktemp").await?;274 cmd.arg("-d");275 let path = cmd.run_string().await?;276 Ok(path.trim_end().to_owned())277 }278 pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {279 let mut cmd = self.cmd("sh").await?;280 cmd.arg("-c")281 .arg("test -e \"$1\" && echo true || echo false")282 .arg("_")283 .arg(path);284 Ok(cmd.run_value().await?)285 }286 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {287 let mut cmd = self.cmd("cat").await?;288 cmd.arg(path);289 cmd.run_bytes().await290 }291 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {292 let mut cmd = self.cmd("cat").await?;293 cmd.arg(path);294 cmd.run_string().await295 }296 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {297 let mut cmd = self.cmd("ls").await?;298 cmd.arg(path);299 let out = cmd.run_string().await?;300 let mut lines = out.split('\n');301 if let Some(last) = lines.next_back() {302 ensure!(last.is_empty(), "output of ls should end with newline");303 }304 Ok(lines.map(ToOwned::to_owned).collect())305 }306 #[allow(dead_code)]307 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {308 let text = self.read_file_text(path).await?;309 Ok(serde_json::from_str(&text)?)310 }311 pub async fn read_env(&self, env: &str) -> Result<String> {312 let mut cmd = self.cmd("printenv").await?;313 cmd.arg(env);314 cmd.run_string().await315 }316 pub async fn find_in_path(&self, command: &str) -> Result<String> {317 // // `which` is not a part of coreutils, and it might not exist on machine.318 // let path = self.read_env("PATH").await?;319 // // Assuming delimiter is :, we don't work with windows host, this check will be much320 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)321 // for ele in path.split(':') {322 // let test_path = format!("{ele}/{cmd}");323 // test -x etc324 // }325 // let mut cmd = self.cmd("printenv").await?;326 // cmd.arg(env);327 // Ok(cmd.run_string().await?)328 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.329 let mut cmd = self330 .cmd_escalation(331 // Not used332 EscalationStrategy::Su,333 "which",334 )335 .await?;336 cmd.arg(command);337 cmd.run_string().await338 }339 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>340 where341 <D as FromStr>::Err: Display,342 {343 let text = self.read_file_text(path).await?;344 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))345 }346 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {347 self.cmd_escalation(self.escalation_strategy().await?, cmd)348 .await349 }350 pub async fn cmd_escalation(351 &self,352 escalation: EscalationStrategy,353 cmd: impl AsRef<OsStr>,354 ) -> Result<MyCommand> {355 if self.local {356 Ok(MyCommand::new(escalation, cmd))357 } else {358 let session = self.open_session().await?;359 Ok(MyCommand::new_on(escalation, cmd, session))360 }361 }362 pub async fn nix_cmd(&self) -> Result<MyCommand> {363 let mut nix = self.cmd("nix").await?;364 nix.args([365 "--extra-experimental-features",366 "nix-command",367 "--extra-experimental-features",368 "flakes",369 ]);370 Ok(nix)371 }372373 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {374 ensure!(data.encrypted, "secret is not encrypted");375 let mut cmd = self.cmd("fleet-install-secrets").await?;376 cmd.arg("decrypt").eqarg("--secret", data.to_string());377 let encoded = cmd378 .sudo()379 .run_string()380 .await381 .context("failed to call remote host for decrypt")?;382 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;383 ensure!(!data.encrypted, "secret came out encrypted");384 Ok(data.data)385 }386 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {387 ensure!(data.encrypted, "secret is not encrypted");388 let mut cmd = self.cmd("fleet-install-secrets").await?;389 cmd.arg("reencrypt").eqarg("--secret", data.to_string());390 for target in targets {391 let key = self.config.key(&target).await?;392 cmd.eqarg("--targets", key);393 }394 let encoded = cmd395 .sudo()396 .run_string()397 .await398 .context("failed to call remote host for decrypt")?;399 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;400 ensure!(data.encrypted, "secret came out not encrypted");401 Ok(data)402 }403 /// Returns path for futureproofing, as path might change i.e on conversion to CA404 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {405 if self.local {406 // Path is located locally, thus already trusted.407 return Ok(path.to_owned());408 }409 let mut nix = MyCommand::new(410 // Not used411 EscalationStrategy::Su,412 "nix",413 );414 nix.arg("copy").arg("--substitute-on-destination");415416 match self.deploy_kind().await? {417 DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {418 nix.comparg("--to", format!("ssh-ng://{}", self.name));419 }420 DeployKind::NixosInstall => {421 nix422 // Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon423 .arg("--no-check-sigs")424 .comparg(425 "--to",426 format!("ssh-ng://root@{}?remote-store=/mnt", self.name),427 );428 }429 }430 nix.arg(path);431 nix.run_nix().await.context("nix copy")?;432 Ok(path.to_owned())433 }434 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {435 let mut cmd = self.cmd("systemctl").await?;436 cmd.arg("stop").arg(name);437 cmd.sudo().run().await438 }439 pub async fn systemctl_start(&self, name: &str) -> Result<()> {440 let mut cmd = self.cmd("systemctl").await?;441 cmd.arg("start").arg(name);442 cmd.sudo().run().await443 }444445 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {446 let mut cmd = self.cmd("rm").await?;447 cmd.arg("-f").arg(path);448 if sudo {449 cmd = cmd.sudo()450 }451 cmd.run().await452 }453}454impl ConfigHost {455 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,456 // assuming getting tags always returns the same value.457 pub async fn tags(&self) -> Result<Vec<String>> {458 if let Some(v) = self.groups.get() {459 return Ok(v.clone());460 }461 let Some(host_config) = &self.host_config else {462 return Ok(vec![]);463 };464 let tags: Vec<String> = nix_go_json!(host_config.tags);465466 let _ = self.groups.set(tags.clone());467468 Ok(tags)469 }470 pub async fn nixos_config(&self) -> Result<Value> {471 if let Some(v) = self.nixos_config.get() {472 return Ok(v.clone());473 }474 let Some(host_config) = &self.host_config else {475 bail!("local host has no nixos_config");476 };477 let nixos_config = nix_go!(host_config.nixos.config);478 assert_warn("nixos config evaluation", &nixos_config).await?;479480 let _ = self.nixos_config.set(nixos_config.clone());481482 Ok(nixos_config)483 }484 pub async fn nixos_unchecked_config(&self) -> Result<Value> {485 if let Some(v) = self.nixos_unchecked_config.get() {486 return Ok(v.clone());487 }488 let Some(host_config) = &self.host_config else {489 bail!("local host has no nixos_config");490 };491 let nixos_config = nix_go!(host_config.nixos_unchecked.config);492493 let _ = self.nixos_unchecked_config.set(nixos_config.clone());494495 Ok(nixos_config)496 }497498 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {499 let nixos = self.nixos_unchecked_config().await?;500 let secrets = nix_go!(nixos.secrets);501 let mut out = Vec::new();502 for name in secrets.list_fields().await? {503 let secret = nix_go!(secrets[{ name }]);504 let is_shared: bool = nix_go_json!(secret.shared);505 if is_shared {506 continue;507 }508 out.push(name);509 }510 Ok(out)511 }512 pub async fn secret_field(&self, name: &str) -> Result<Value> {513 let nixos = self.nixos_unchecked_config().await?;514 Ok(nix_go!(nixos.secrets[{ name }]))515 }516517 /// Packages for this host, resolved with nixpkgs overlays518 pub async fn pkgs(&self) -> Result<Value> {519 if let Some(value) = &self.pkgs_override {520 return Ok(value.clone());521 }522 let Some(host_config) = &self.host_config else {523 bail!("local host has no host_config");524 };525 // TODO: Should nixos.options be cached?526 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))527 }528}529530impl Config {531 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {532 let config = &self.config_field;533 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);534 Ok(tagged)535 }536 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {537 let mut out = BTreeSet::new();538 for owner in owners {539 if let Some(tag) = owner.strip_prefix('@') {540 let hosts = self.tagged_hostnames(tag).await?;541 out.extend(hosts);542 } else {543 out.insert(owner);544 }545 }546 Ok(out)547 }548 pub fn local_host(&self) -> ConfigHost {549 ConfigHost {550 config: self.clone(),551 name: "<virtual localhost>".to_owned(),552 host_config: None,553 nixos_config: OnceCell::new(),554 nixos_unchecked_config: OnceCell::new(),555 groups: {556 let cell = OnceCell::new();557 let _ = cell.set(vec![]);558 cell559 },560 pkgs_override: Some(self.default_pkgs.clone()),561562 local: true,563 session: OnceLock::new(),564 deploy_kind: OnceCell::new(),565 }566 }567568 pub async fn host(&self, name: &str) -> Result<ConfigHost> {569 let config = &self.config_field;570 let host_config = nix_go!(config.hosts[{ name }]);571572 Ok(ConfigHost {573 config: self.clone(),574 name: name.to_owned(),575 host_config: Some(host_config),576 nixos_config: OnceCell::new(),577 nixos_unchecked_config: OnceCell::new(),578 groups: OnceCell::new(),579 pkgs_override: None,580581 // TODO: Remove with connectivit refactor582 local: self.localhost == name,583 session: OnceLock::new(),584 deploy_kind: OnceCell::new(),585 })586 }587 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {588 let config = &self.config_field;589 let names = nix_go!(config.hosts).list_fields().await?;590 let mut out = vec![];591 for name in names {592 out.push(self.host(&name).await?);593 }594 Ok(out)595 }596 // TODO: Replace usages with .host().nixos_config597 pub async fn system_config(&self, host: &str) -> Result<Value> {598 let fleet_field = &self.config_field;599 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))600 }601602 /// Shared secrets configured in fleet.nix or in flake603 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {604 let config_field = &self.config_field;605 Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)606 }607 /// Shared secrets configured in fleet.nix608 pub fn list_shared(&self) -> Vec<String> {609 let data = self.data();610 data.shared_secrets.keys().cloned().collect()611 }612 pub fn has_shared(&self, name: &str) -> bool {613 let data = self.data();614 data.shared_secrets.contains_key(name)615 }616 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {617 let mut data = self.data_mut();618 data.shared_secrets.insert(name.to_owned(), shared);619 }620 pub fn remove_shared(&self, secret: &str) {621 let mut data = self.data_mut();622 data.shared_secrets.remove(secret);623 }624625 pub fn list_secrets(&self, host: &str) -> Vec<String> {626 let data = self.data();627 let Some(secrets) = data.host_secrets.get(host) else {628 return Vec::new();629 };630 secrets.keys().cloned().collect()631 }632633 pub fn has_secret(&self, host: &str, secret: &str) -> bool {634 let data = self.data();635 let Some(host_secrets) = data.host_secrets.get(host) else {636 return false;637 };638 host_secrets.contains_key(secret)639 }640 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {641 let mut data = self.data_mut();642 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();643 host_secrets.insert(secret, value);644 }645646 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {647 let data = self.data();648 let Some(host_secrets) = data.host_secrets.get(host) else {649 bail!("no secrets for machine {host}");650 };651 let Some(secret) = host_secrets.get(secret) else {652 bail!("machine {host} has no secret {secret}");653 };654 Ok(secret.clone())655 }656 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {657 let data = self.data();658 let Some(secret) = data.shared_secrets.get(secret) else {659 bail!("no shared secret {secret}");660 };661 Ok(secret.clone())662 }663 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {664 let config_field = &self.config_field;665 Ok(nix_go_json!(666 config_field.sharedSecrets[{ secret }].expectedOwners667 ))668 }669670 // TODO: Should this be something modifiable from other processes?671 // E.g terraform provider might want to update FleetData (e.g secrets),672 // and current implementation assumes only one process holds current fleet.nix673 // Given that it is no longer needs to be a file for nix evaluation,674 // maybe it can be a .nix file for persistence, but accessible only675 // thru some shared state controller? Might it be stored in terraform676 // state provider?677 pub fn data(&self) -> MutexGuard<FleetData> {678 self.data.lock().unwrap()679 }680 pub fn data_mut(&self) -> MutexGuard<FleetData> {681 self.data.lock().unwrap()682 }683 pub fn save(&self) -> Result<()> {684 let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;685 let data = nixlike::serialize(&self.data() as &FleetData)?;686 tempfile.write_all(687 format!(688 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",689 data690 )691 .as_bytes(),692 )?;693 let mut fleet_data_path = self.directory.clone();694 fleet_data_path.push("fleet.nix");695 tempfile.persist(fleet_data_path)?;696 Ok(())697 }698}crates/fleet-base/src/keys.rsdiffbeforeafterboth--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -1,7 +1,7 @@
use std::str::FromStr as _;
use age::Recipient;
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
use futures::{StreamExt as _, TryStreamExt as _};
use itertools::Itertools as _;
use tracing::warn;
@@ -39,12 +39,12 @@
}
}
/// Insecure, requires root
- pub async fn recipient(&self, host: &str) -> anyhow::Result<impl Recipient> {
+ pub async fn recipient(&self, host: &str) -> anyhow::Result<impl Recipient + use<>> {
let key = self.key(host).await?;
age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
}
- pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient>> {
+ pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient + use<>>> {
let hosts = self.expand_owner_set(hosts).await?;
futures::stream::iter(hosts.iter())
.then(|m| self.recipient(m.as_ref()))
crates/fleet-base/src/lib.rsdiffbeforeafterboth--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -1,6 +1,6 @@
pub mod command;
+pub mod deploy;
pub mod fleetdata;
pub mod host;
mod keys;
pub mod opts;
-pub mod deploy;
\ No newline at end of file
crates/fleet-base/src/opts.rsdiffbeforeafterboth--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -6,15 +6,15 @@
sync::{Arc, Mutex},
};
-use anyhow::{bail, Context, Result};
-use nix_eval::{nix_go, util::assert_warn, NixSessionPool, Value};
+use anyhow::{Context, Result, bail};
+use nix_eval::{NixSessionPool, Value, nix_go, util::assert_warn};
use nom::{
+ Parser,
bytes::complete::take_while1,
character::complete::char,
combinator::{map, opt},
multi::separated_list1,
sequence::{preceded, separated_pair},
- Parser,
};
use crate::{
@@ -44,7 +44,8 @@
let (input, name) = map(
take_while1(|v| v != ',' && v != '?' && v != '@'),
str::to_owned,
- ).parse_complete(input)
+ )
+ .parse_complete(input)
.map_err(err_to_string)?;
let kw_item = separated_pair(
crates/fleet-shared/src/encoding.rsdiffbeforeafterboth--- a/crates/fleet-shared/src/encoding.rs
+++ b/crates/fleet-shared/src/encoding.rs
@@ -3,8 +3,8 @@
str::FromStr,
};
-use base64::engine::{general_purpose::STANDARD_NO_PAD, Engine};
-use serde::{de::Error, Deserialize, Deserializer, Serialize};
+use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};
+use serde::{Deserialize, Deserializer, Serialize, de::Error};
use unicode_categories::UnicodeCategories;
#[derive(Debug, PartialEq, Clone)]
crates/nix-eval/src/pool.rsdiffbeforeafterboth--- a/crates/nix-eval/src/pool.rs
+++ b/crates/nix-eval/src/pool.rs
@@ -5,7 +5,7 @@
use r2d2::Pool;
-use crate::{session::NixSessionInner, Error, NixSession, Result};
+use crate::{Error, NixSession, Result, session::NixSessionInner};
pub struct NixSessionPool(Pool<NixSessionPoolInner>);
impl NixSessionPool {
crates/nix-eval/src/session.rsdiffbeforeafterboth--- a/crates/nix-eval/src/session.rs
+++ b/crates/nix-eval/src/session.rs
@@ -3,16 +3,16 @@
use better_command::{ClonableHandler, Handler, NixHandler, NoopHandler};
use futures::StreamExt;
use itertools::Itertools as _;
-use serde::{de::DeserializeOwned, Deserialize};
+use serde::{Deserialize, de::DeserializeOwned};
use thiserror::Error;
use tokio::{
io::AsyncWriteExt,
process::{ChildStderr, ChildStdin, ChildStdout, Command},
select,
- sync::{mpsc, oneshot, Mutex},
+ sync::{Mutex, mpsc, oneshot},
};
use tokio_util::codec::{FramedRead, LinesCodec};
-use tracing::{debug, error, warn, Level};
+use tracing::{Level, debug, error, warn};
#[derive(Error, Debug, Clone)]
pub enum Error {
crates/nix-eval/src/util.rsdiffbeforeafterboth--- a/crates/nix-eval/src/util.rs
+++ b/crates/nix-eval/src/util.rs
@@ -3,7 +3,7 @@
use anyhow::bail;
use tracing::{debug, warn};
-use crate::{nix_go_json, Value};
+use crate::{Value, nix_go_json};
#[tracing::instrument(level = "info", skip(val))]
pub async fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
crates/nix-eval/src/value.rsdiffbeforeafterboth--- a/crates/nix-eval/src/value.rs
+++ b/crates/nix-eval/src/value.rs
@@ -1,9 +1,9 @@
use std::{collections::HashMap, fmt, path::PathBuf, sync::Arc};
use better_command::NixHandler;
-use serde::{de::DeserializeOwned, Serialize};
+use serde::{Serialize, de::DeserializeOwned};
-use crate::{macros::NixExprBuilder, nix_go, Error, NixBuildBatch, NixSession, Result};
+use crate::{Error, NixBuildBatch, NixSession, Result, macros::NixExprBuilder, nix_go};
#[derive(Clone)]
pub enum Index {
crates/nix-native-eval/Cargo.tomldiffbeforeafterboth--- a/crates/nix-native-eval/Cargo.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-[package]
-name = "nix-native-eval"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-
-nixrs = { git = "https://github.com/Anillc/nixrs", version = "0.1.0" }
crates/nix-native-eval/src/lib.rsdiffbeforeafterboth--- a/crates/nix-native-eval/src/lib.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use anyhow::Result;
-use nixrs::{State, Store};
-
-pub fn init() -> Result<()> {
- nixrs::init()?;
- let store = Store::new("daemon")?;
- let state = State::new(store)?;
- let _ = state;
-
- Ok(())
-}
crates/nixlike/fuzz/Cargo.tomldiffbeforeafterboth--- a/crates/nixlike/fuzz/Cargo.toml
+++ b/crates/nixlike/fuzz/Cargo.toml
@@ -4,7 +4,7 @@
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
-edition = "2021"
+edition = "2024"
[package.metadata]
cargo-fuzz = true
crates/nixlike/src/de_impl.rsdiffbeforeafterboth--- a/crates/nixlike/src/de_impl.rs
+++ b/crates/nixlike/src/de_impl.rs
@@ -2,8 +2,8 @@
use linked_hash_map::LinkedHashMap;
use serde::{
+ Deserializer,
de::{self, MapAccess, SeqAccess},
- Deserializer,
};
use crate::{Error, Value};
@@ -28,11 +28,12 @@
where
K: de::DeserializeSeed<'de>,
{
- if let Some((k, v)) = self.iter.next() {
- let _ = self.value.insert(v);
- Ok(Some(seed.deserialize(Value::String(k))?))
- } else {
- Ok(None)
+ match self.iter.next() {
+ Some((k, v)) => {
+ let _ = self.value.insert(v);
+ Ok(Some(seed.deserialize(Value::String(k))?))
+ }
+ _ => Ok(None),
}
}
@@ -62,10 +63,9 @@
where
T: de::DeserializeSeed<'de>,
{
- if let Some(v) = self.iter.next() {
- Ok(Some(seed.deserialize(v)?))
- } else {
- Ok(None)
+ match self.iter.next() {
+ Some(v) => Ok(Some(seed.deserialize(v)?)),
+ _ => Ok(None),
}
}
}
crates/nixlike/src/se_impl.rsdiffbeforeafterboth--- a/crates/nixlike/src/se_impl.rs
+++ b/crates/nixlike/src/se_impl.rs
@@ -2,11 +2,11 @@
use linked_hash_map::LinkedHashMap;
use serde::{
+ Serializer,
ser::{
self, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
SerializeTupleStruct, SerializeTupleVariant,
},
- Serializer,
};
use crate::{Error, Value};
@@ -90,9 +90,7 @@
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(Value::Object(
- vec![(self.0, Value::Array(self.1 .0))]
- .into_iter()
- .collect(),
+ vec![(self.0, Value::Array(self.1.0))].into_iter().collect(),
))
}
}
flake.lockdiffbeforeafterboth--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"crane": {
"locked": {
- "lastModified": 1739936662,
- "narHash": "sha256-x4syUjNUuRblR07nDPeLDP7DpphaBVbUaSoeZkFbGSk=",
+ "lastModified": 1750266157,
+ "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
"owner": "ipetkov",
"repo": "crane",
- "rev": "19de14aaeb869287647d9461cbd389187d8ecdb7",
+ "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48",
"type": "github"
},
"original": {
@@ -22,11 +22,11 @@
]
},
"locked": {
- "lastModified": 1738453229,
- "narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=",
+ "lastModified": 1749398372,
+ "narHash": "sha256-tYBdgS56eXYaWVW3fsnPQ/nFlgWi/Z2Ymhyu21zVM98=",
"owner": "hercules-ci",
"repo": "flake-parts",
- "rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd",
+ "rev": "9305fe4e5c2a6fcf5ba6a3ff155720fbe4076569",
"type": "github"
},
"original": {
@@ -37,16 +37,16 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1740339700,
- "narHash": "sha256-cbrw7EgQhcdFnu6iS3vane53bEagZQy/xyIkDWpCgVE=",
+ "lastModified": 1750895632,
+ "narHash": "sha256-EPZWiRmaSTxoBArK5dQyRlSNVLXiBt2hmsYIPgMf3zk=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "04ef94c4c1582fd485bbfdb8c4a8ba250e359195",
+ "rev": "6ac57ce7fee0d80226095a57ccb7519855ad7c5e",
"type": "github"
},
"original": {
"owner": "nixos",
- "ref": "release-24.11",
+ "ref": "release-25.05",
"repo": "nixpkgs",
"type": "github"
}
@@ -68,11 +68,11 @@
]
},
"locked": {
- "lastModified": 1740277845,
- "narHash": "sha256-NNU0CdiaSbAeZ8tpDG4aFi9qtcdlItRvk8Xns9oBrVU=",
+ "lastModified": 1750819193,
+ "narHash": "sha256-XvkupGPZqD54HuKhN/2WhbKjAHeTl1UEnWspzUzRFfA=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "f933070c29f9c1c5457447a51903f27f76ebb519",
+ "rev": "1ba3b9c59b68a4b00156827ad46393127b51b808",
"type": "github"
},
"original": {
@@ -103,11 +103,11 @@
]
},
"locked": {
- "lastModified": 1744961264,
- "narHash": "sha256-aRmUh0AMwcbdjJHnytg1e5h5ECcaWtIFQa6d9gI85AI=",
+ "lastModified": 1749194973,
+ "narHash": "sha256-eEy8cuS0mZ2j/r/FE0/LYBSBcIs/MKOIVakwHVuqTfk=",
"owner": "numtide",
"repo": "treefmt-nix",
- "rev": "8d404a69efe76146368885110f29a2ca3700bee6",
+ "rev": "a05be418a1af1198ca0f63facb13c985db4cb3c5",
"type": "github"
},
"original": {
flake.nixdiffbeforeafterboth--- a/flake.nix
+++ b/flake.nix
@@ -2,7 +2,7 @@
description = "NixOS cluster configuration management";
inputs = {
- nixpkgs.url = "github:nixos/nixpkgs/release-24.11";
+ nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
@@ -156,7 +156,7 @@
bacon
nil
rustPlatform.bindgenHook
- nixVersions.nix_2_22
+ # nixVersions.nix_2_22
];
environment.PROTOC = "${pkgs.protobuf}/bin/protoc";
};
rust-toolchain.tomldiffbeforeafterboth--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
[toolchain]
-channel = "1.85.0"
+channel = "1.86.0"
components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]
rustfmt.tomldiffbeforeafterboth--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1,3 @@
hard_tabs = true
-imports_granularity = "Crate"
-group_imports = "StdExternalCrate"
+# imports_granularity = "Crate"
+# group_imports = "StdExternalCrate"