difftreelog
fix legacy ssh store support
in: trunk
4 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
@@ -106,6 +106,9 @@
if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {
host.set_session_destination(destination);
};
+ if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {
+ host.set_legacy_ssh_store(legacy);
+ };
set.spawn_local(
(async move {
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::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{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,44}4546// TODO: Make field not pub47#[derive(Clone)]48pub struct Config(pub Arc<FleetConfigInternals>);4950impl Deref for Config {51 type Target = FleetConfigInternals;5253 fn deref(&self) -> &Self::Target {54 &self.055 }56}5758#[derive(Clone, Copy, Debug)]59pub enum EscalationStrategy {60 Sudo,61 Run0,62 Su,63}6465#[derive(Clone, PartialEq, Copy, Debug)]66pub enum DeployKind {67 /// NixOS => NixOS managed by fleet68 UpgradeToFleet,69 /// NixOS managed by fleet => NixOS managed by fleet70 Fleet,71 /// Remote host has /mnt, /mnt/boot mounted,72 /// generated config is added to fleet configuration.73 NixosInstall,74 /// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),75 /// generated config is added to fleet configuration,76 /// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.77 NixosLustrate,78}7980impl FromStr for DeployKind {81 type Err = anyhow::Error;82 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {83 match s {84 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),85 "fleet" => Ok(Self::Fleet),86 "nixos-install" => Ok(Self::NixosInstall),87 "nixos-lustrate" => Ok(Self::NixosLustrate),88 v => bail!(89 "unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""90 ),91 }92 }93}94pub struct ConfigHost {95 config: Config,96 pub name: String,97 groups: OnceCell<Vec<String>>,9899 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it100 deploy_kind: OnceCell<DeployKind>,101 session_destination: OnceCell<String>,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_session_destination(&self, dest: String) {213 self.session_destination214 .set(dest)215 .expect("session destination is already set")216 }217 pub fn set_deploy_kind(&self, kind: DeployKind) {218 self.deploy_kind219 .set(kind)220 .expect("deploy kind is already set");221 }222 pub async fn deploy_kind(&self) -> Result<DeployKind> {223 if let Some(kind) = self.deploy_kind.get() {224 return Ok(*kind);225 }226 let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {227 Ok(v) => v,228 Err(e) => {229 bail!("failed to query remote system kind: {}", e);230 }231 };232 if !is_fleet_managed {233 bail!(234 "{}",235 indoc::indoc! {"236 host is not marked as managed by fleet237 if you're not trying to lustrate/install system from scratch,238 you should either239 1. manually create /etc/FLEET_HOST file on the target host,240 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet241 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos242 "}243 );244 }245 // TOCTOU is possible246 let _ = self.deploy_kind.set(DeployKind::Fleet);247 Ok(*self.deploy_kind.get().expect("deploy kind is just set"))248 }249 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {250 // Prefer sudo, as run0 has some gotchas with polkit251 // and too many repeating prompts.252 if (self.find_in_path("sudo").await).is_ok() {253 return Ok(EscalationStrategy::Sudo);254 }255 if (self.find_in_path("run0").await).is_ok() {256 return Ok(EscalationStrategy::Run0);257 }258 Ok(EscalationStrategy::Su)259 }260 async fn open_session(&self) -> Result<Arc<openssh::Session>> {261 assert!(!self.local, "do not open ssh connection to local session");262 // FIXME: TOCTOU263 if let Some(session) = &self.session.get() {264 return Ok((*session).clone());265 };266 let session = SessionBuilder::default();267268 let dest = self.session_destination.get().unwrap_or(&self.name);269 let session = session270 .connect(&dest)271 .await272 .map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;273 let session = Arc::new(session);274 self.session.set(session.clone()).expect("TOCTOU happened");275 Ok(session)276 }277 pub async fn mktemp_dir(&self) -> Result<String> {278 let mut cmd = self.cmd("mktemp").await?;279 cmd.arg("-d");280 let path = cmd.run_string().await?;281 Ok(path.trim_end().to_owned())282 }283 pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {284 let mut cmd = self.cmd("sh").await?;285 cmd.arg("-c")286 .arg("test -e \"$1\" && echo true || echo false")287 .arg("_")288 .arg(path);289 cmd.run_value().await290 }291 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {292 let mut cmd = self.cmd("cat").await?;293 cmd.arg(path);294 cmd.run_bytes().await295 }296 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {297 let mut cmd = self.cmd("cat").await?;298 cmd.arg(path);299 cmd.run_string().await300 }301 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {302 let mut cmd = self.cmd("ls").await?;303 cmd.arg(path);304 let out = cmd.run_string().await?;305 let mut lines = out.split('\n');306 if let Some(last) = lines.next_back() {307 ensure!(last.is_empty(), "output of ls should end with newline");308 }309 Ok(lines.map(ToOwned::to_owned).collect())310 }311 #[allow(dead_code)]312 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {313 let text = self.read_file_text(path).await?;314 Ok(serde_json::from_str(&text)?)315 }316 pub async fn read_env(&self, env: &str) -> Result<String> {317 let mut cmd = self.cmd("printenv").await?;318 cmd.arg(env);319 cmd.run_string().await320 }321 pub async fn find_in_path(&self, command: &str) -> Result<String> {322 // // `which` is not a part of coreutils, and it might not exist on machine.323 // let path = self.read_env("PATH").await?;324 // // Assuming delimiter is :, we don't work with windows host, this check will be much325 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)326 // for ele in path.split(':') {327 // let test_path = format!("{ele}/{cmd}");328 // test -x etc329 // }330 // let mut cmd = self.cmd("printenv").await?;331 // cmd.arg(env);332 // Ok(cmd.run_string().await?)333 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.334 let mut cmd = self335 .cmd_escalation(336 // Not used337 EscalationStrategy::Su,338 "which",339 )340 .await?;341 cmd.arg(command);342 cmd.run_string().await343 }344 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>345 where346 <D as FromStr>::Err: Display,347 {348 let text = self.read_file_text(path).await?;349 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))350 }351 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {352 self.cmd_escalation(self.escalation_strategy().await?, cmd)353 .await354 }355 pub async fn cmd_escalation(356 &self,357 escalation: EscalationStrategy,358 cmd: impl AsRef<OsStr>,359 ) -> Result<MyCommand> {360 if self.local {361 Ok(MyCommand::new(escalation, cmd))362 } else {363 let session = self.open_session().await?;364 Ok(MyCommand::new_on(escalation, cmd, session))365 }366 }367 pub async fn nix_cmd(&self) -> Result<MyCommand> {368 let mut nix = self.cmd("nix").await?;369 nix.args([370 "--extra-experimental-features",371 "nix-command",372 "--extra-experimental-features",373 "flakes",374 ]);375 Ok(nix)376 }377378 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {379 ensure!(data.encrypted, "secret is not encrypted");380 let mut cmd = self.cmd("fleet-install-secrets").await?;381 cmd.arg("decrypt").eqarg("--secret", data.to_string());382 let encoded = cmd383 .sudo()384 .run_string()385 .await386 .context("failed to call remote host for decrypt")?;387 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;388 ensure!(!data.encrypted, "secret came out encrypted");389 Ok(data.data)390 }391 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {392 ensure!(data.encrypted, "secret is not encrypted");393 let mut cmd = self.cmd("fleet-install-secrets").await?;394 cmd.arg("reencrypt").eqarg("--secret", data.to_string());395 for target in targets {396 let key = self.config.key(&target).await?;397 cmd.eqarg("--targets", key);398 }399 let encoded = cmd400 .sudo()401 .run_string()402 .await403 .context("failed to call remote host for decrypt")?;404 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;405 ensure!(data.encrypted, "secret came out not encrypted");406 Ok(data)407 }408 /// Returns path for futureproofing, as path might change i.e on conversion to CA409 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {410 if self.local {411 // Path is located locally, thus already trusted.412 return Ok(path.to_owned());413 }414 let mut nix = MyCommand::new(415 // Not used416 EscalationStrategy::Su,417 "nix",418 );419 nix.arg("copy").arg("--substitute-on-destination");420421 match self.deploy_kind().await? {422 DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {423 nix.comparg("--to", format!("ssh-ng://{}", self.name));424 }425 DeployKind::NixosInstall => {426 nix427 // Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon428 .arg("--no-check-sigs")429 .comparg(430 "--to",431 format!("ssh-ng://root@{}?remote-store=/mnt", self.name),432 );433 }434 }435 nix.arg(path);436 nix.run_nix().await.context("nix copy")?;437 Ok(path.to_owned())438 }439 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {440 let mut cmd = self.cmd("systemctl").await?;441 cmd.arg("stop").arg(name);442 cmd.sudo().run().await443 }444 pub async fn systemctl_start(&self, name: &str) -> Result<()> {445 let mut cmd = self.cmd("systemctl").await?;446 cmd.arg("start").arg(name);447 cmd.sudo().run().await448 }449450 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {451 let mut cmd = self.cmd("rm").await?;452 cmd.arg("-f").arg(path);453 if sudo {454 cmd = cmd.sudo()455 }456 cmd.run().await457 }458}459impl ConfigHost {460 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,461 // assuming getting tags always returns the same value.462 pub async fn tags(&self) -> Result<Vec<String>> {463 if let Some(v) = self.groups.get() {464 return Ok(v.clone());465 }466 let Some(host_config) = &self.host_config else {467 return Ok(vec![]);468 };469 let tags: Vec<String> = nix_go_json!(host_config.tags);470471 let _ = self.groups.set(tags.clone());472473 Ok(tags)474 }475 pub async fn nixos_config(&self) -> Result<Value> {476 if let Some(v) = self.nixos_config.get() {477 return Ok(v.clone());478 }479 let Some(host_config) = &self.host_config else {480 bail!("local host has no nixos_config");481 };482 let nixos_config = nix_go!(host_config.nixos.config);483 assert_warn("nixos config evaluation", &nixos_config).await?;484485 let _ = self.nixos_config.set(nixos_config.clone());486487 Ok(nixos_config)488 }489 pub async fn nixos_unchecked_config(&self) -> Result<Value> {490 if let Some(v) = self.nixos_unchecked_config.get() {491 return Ok(v.clone());492 }493 let Some(host_config) = &self.host_config else {494 bail!("local host has no nixos_config");495 };496 let nixos_config = nix_go!(host_config.nixos_unchecked.config);497498 let _ = self.nixos_unchecked_config.set(nixos_config.clone());499500 Ok(nixos_config)501 }502503 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {504 let nixos = self.nixos_unchecked_config().await?;505 let secrets = nix_go!(nixos.secrets);506 let mut out = Vec::new();507 for name in secrets.list_fields()? {508 let secret = secrets.get_field(&name)?;509 let is_shared: bool = nix_go_json!(secret.shared);510 if is_shared {511 continue;512 }513 out.push(name);514 }515 Ok(out)516 }517 pub async fn secret_field(&self, name: &str) -> Result<Value> {518 let nixos = self.nixos_unchecked_config().await?;519 Ok(nix_go!(nixos.secrets[{ name }]))520 }521522 /// Packages for this host, resolved with nixpkgs overlays523 pub async fn pkgs(&self) -> Result<Value> {524 if let Some(value) = &self.pkgs_override {525 return Ok(value.clone());526 }527 let Some(host_config) = &self.host_config else {528 bail!("local host has no host_config");529 };530 // TODO: Should nixos.options be cached?531 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))532 }533}534535impl Config {536 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {537 let config = &self.config_field;538 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);539 Ok(tagged)540 }541 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {542 let mut out = BTreeSet::new();543 for owner in owners {544 if let Some(tag) = owner.strip_prefix('@') {545 let hosts = self.tagged_hostnames(tag).await?;546 out.extend(hosts);547 } else {548 out.insert(owner);549 }550 }551 Ok(out)552 }553 pub fn local_host(&self) -> ConfigHost {554 ConfigHost {555 config: self.clone(),556 name: "<virtual localhost>".to_owned(),557 host_config: None,558 nixos_config: OnceCell::new(),559 nixos_unchecked_config: OnceCell::new(),560 groups: {561 let cell = OnceCell::new();562 let _ = cell.set(vec![]);563 cell564 },565 pkgs_override: Some(self.default_pkgs.clone()),566567 local: true,568 session: OnceLock::new(),569 deploy_kind: OnceCell::new(),570 session_destination: OnceCell::new(),571 }572 }573574 pub async fn host(&self, name: &str) -> Result<ConfigHost> {575 let config = &self.config_field;576 let host_config = nix_go!(config.hosts[{ name }]);577578 Ok(ConfigHost {579 config: self.clone(),580 name: name.to_owned(),581 host_config: Some(host_config),582 nixos_config: OnceCell::new(),583 nixos_unchecked_config: OnceCell::new(),584 groups: OnceCell::new(),585 pkgs_override: None,586587 // TODO: Remove with connectivit refactor588 local: self.localhost == name,589 session: OnceLock::new(),590 deploy_kind: OnceCell::new(),591 session_destination: OnceCell::new(),592 })593 }594 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {595 let config = &self.config_field;596 let names = nix_go!(config.hosts).list_fields()?;597 let mut out = vec![];598 for name in names {599 out.push(self.host(&name).await?);600 }601 Ok(out)602 }603 // TODO: Replace usages with .host().nixos_config604 pub async fn system_config(&self, host: &str) -> Result<Value> {605 let fleet_field = &self.config_field;606 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))607 }608609 /// Shared secrets configured in fleet.nix or in flake610 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {611 let config_field = &self.config_field;612 nix_go!(config_field.sharedSecrets).list_fields()613 }614 /// Shared secrets configured in fleet.nix615 pub fn list_shared(&self) -> Vec<String> {616 let data = self.data();617 data.shared_secrets.keys().cloned().collect()618 }619 pub fn has_shared(&self, name: &str) -> bool {620 let data = self.data();621 data.shared_secrets.contains_key(name)622 }623 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {624 let mut data = self.data_mut();625 data.shared_secrets.insert(name.to_owned(), shared);626 }627 pub fn remove_shared(&self, secret: &str) {628 let mut data = self.data_mut();629 data.shared_secrets.remove(secret);630 }631632 pub fn list_secrets(&self, host: &str) -> Vec<String> {633 let data = self.data();634 let Some(secrets) = data.host_secrets.get(host) else {635 return Vec::new();636 };637 secrets.keys().cloned().collect()638 }639640 pub fn has_secret(&self, host: &str, secret: &str) -> bool {641 let data = self.data();642 let Some(host_secrets) = data.host_secrets.get(host) else {643 return false;644 };645 host_secrets.contains_key(secret)646 }647 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {648 let mut data = self.data_mut();649 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();650 host_secrets.insert(secret, value);651 }652653 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {654 let data = self.data();655 let Some(host_secrets) = data.host_secrets.get(host) else {656 bail!("no secrets for machine {host}");657 };658 let Some(secret) = host_secrets.get(secret) else {659 bail!("machine {host} has no secret {secret}");660 };661 Ok(secret.clone())662 }663 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {664 let data = self.data();665 let Some(secret) = data.shared_secrets.get(secret) else {666 bail!("no shared secret {secret}");667 };668 Ok(secret.clone())669 }670 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {671 let config_field = &self.config_field;672 Ok(nix_go_json!(673 config_field.sharedSecrets[{ secret }].expectedOwners674 ))675 }676677 // TODO: Should this be something modifiable from other processes?678 // E.g terraform provider might want to update FleetData (e.g secrets),679 // and current implementation assumes only one process holds current fleet.nix680 // Given that it is no longer needs to be a file for nix evaluation,681 // maybe it can be a .nix file for persistence, but accessible only682 // thru some shared state controller? Might it be stored in terraform683 // state provider?684 pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {685 self.data.lock().unwrap()686 }687 pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {688 self.data.lock().unwrap()689 }690 pub fn save(&self) -> Result<()> {691 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.")?;692 let data = nixlike::serialize(&self.data() as &FleetData)?;693 tempfile.write_all(694 format!(695 "# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"696 )697 .as_bytes(),698 )?;699 let mut fleet_data_path = self.directory.clone();700 fleet_data_path.push("fleet.nix");701 tempfile.persist(fleet_data_path)?;702 Ok(())703 }704}crates/fleet-shared/src/encoding.rsdiffbeforeafterboth--- a/crates/fleet-shared/src/encoding.rs
+++ b/crates/fleet-shared/src/encoding.rs
@@ -1,6 +1,5 @@
use std::{
- fmt::{self, Display},
- str::FromStr,
+ collections::BTreeMap, fmt::{self, Display}, str::FromStr
};
use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};
flake.nixdiffbeforeafterboth--- a/flake.nix
+++ b/flake.nix
@@ -168,12 +168,9 @@
cargo-fuzz
cargo-watch
cargo-outdated
- gdb
pkg-config
openssl
- bacon
- nil
rustPlatform.bindgenHook
inputs'.nix.packages.nix-expr-c
inputs'.nix.packages.nix-flake-c