git.delta.rocks / jrsonnet / refs/commits / 3a7032e3bf89

difftreelog

fix legacy ssh store support

lwkltrupYaroslav Bolyukin2025-09-15parent: #79b689b.patch.diff
in: trunk

4 files changed

modifiedcmds/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 {
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -13,7 +13,7 @@
 use anyhow::{Context, Result, anyhow, bail, ensure};
 use fleet_shared::SecretData;
 use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};
-use openssh::SessionBuilder;
+use openssh::{ControlPersist, SessionBuilder};
 use serde::de::DeserializeOwned;
 use tabled::Tabled;
 use tempfile::NamedTempFile;
@@ -99,6 +99,7 @@
 	// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it
 	deploy_kind: OnceCell<DeployKind>,
 	session_destination: OnceCell<String>,
+	legacy_ssh_store: OnceCell<bool>,
 
 	pub host_config: Option<Value>,
 	pub nixos_config: OnceCell<Value>,
@@ -219,6 +220,11 @@
 			.set(kind)
 			.expect("deploy kind is already set");
 	}
+	pub fn set_legacy_ssh_store(&self, legacy: bool) {
+		self.legacy_ssh_store
+			.set(legacy)
+			.expect("legacy ssh store is already set")
+	}
 	pub async fn deploy_kind(&self) -> Result<DeployKind> {
 		if let Some(kind) = self.deploy_kind.get() {
 			return Ok(*kind);
@@ -263,7 +269,8 @@
 		if let Some(session) = &self.session.get() {
 			return Ok((*session).clone());
 		};
-		let session = SessionBuilder::default();
+		let mut session = SessionBuilder::default();
+		session.control_persist(ControlPersist::ClosedAfterInitialConnection);
 
 		let dest = self.session_destination.get().unwrap_or(&self.name);
 		let session = session
@@ -418,9 +425,15 @@
 		);
 		nix.arg("copy").arg("--substitute-on-destination");
 
+		let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {
+			"ssh"
+		} else {
+			"ssh-ng"
+		};
+
 		match self.deploy_kind().await? {
 			DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {
-				nix.comparg("--to", format!("ssh-ng://{}", self.name));
+				nix.comparg("--to", format!("{proto}://{}", self.name));
 			}
 			DeployKind::NixosInstall => {
 				nix
@@ -428,7 +441,7 @@
 					.arg("--no-check-sigs")
 					.comparg(
 						"--to",
-						format!("ssh-ng://root@{}?remote-store=/mnt", self.name),
+						format!("{proto}://root@{}?remote-store=/mnt", self.name),
 					);
 			}
 		}
@@ -568,6 +581,7 @@
 			session: OnceLock::new(),
 			deploy_kind: OnceCell::new(),
 			session_destination: OnceCell::new(),
+			legacy_ssh_store: OnceCell::new(),
 		}
 	}
 
@@ -589,6 +603,7 @@
 			session: OnceLock::new(),
 			deploy_kind: OnceCell::new(),
 			session_destination: OnceCell::new(),
+			legacy_ssh_store: OnceCell::new(),
 		})
 	}
 	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
modifiedcrates/fleet-shared/src/encoding.rsdiffbeforeafterboth
before · crates/fleet-shared/src/encoding.rs
1use std::{2	fmt::{self, Display},3	str::FromStr,4};56use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};7use serde::{Deserialize, Deserializer, Serialize, de::Error};8use unicode_categories::UnicodeCategories;910#[derive(Debug, PartialEq, Clone)]11pub struct SecretData {12	pub data: Vec<u8>,13	pub encrypted: bool,14}1516const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";17const Z85_ENCODED_PREFIX: &str = "<Z85-ENCODED>\n";18// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.19const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";20const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";2122const SECRET_PREFIX: &str = "<ENCRYPTED>";2324impl<'de> Deserialize<'de> for SecretData {25	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>26	where27		D: Deserializer<'de>,28	{29		let string = String::deserialize(deserializer)?;30		string.parse().map_err(D::Error::custom)31	}32}3334impl Serialize for SecretData {35	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>36	where37		S: serde::Serializer,38	{39		self.to_string().serialize(serializer)40	}41}4243impl FromStr for SecretData {44	type Err = String;4546	fn from_str(string: &str) -> Result<Self, Self::Err> {47		let (encrypted, string) = if let Some(unprefixed) = string.strip_prefix(SECRET_PREFIX) {48			(true, unprefixed)49		} else {50			(false, string)51		};52		let data = if let Some(unprefixed) = string.strip_prefix(BASE64_ENCODED_PREFIX) {53			STANDARD_NO_PAD54				.decode(unprefixed.replace(['\n', '\t', ' '], ""))55				.map_err(|e| format!("base64-encoded failed: {e}"))?56		} else if let Some(unprefixed) = string.strip_prefix(Z85_ENCODED_PREFIX) {57			z85::decode(unprefixed.replace(['\n', '\t', ' '], ""))58				.map_err(|e| format!("z85-encoded failed: {e}"))?59		} else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {60			unprefixed.as_bytes().to_owned()61		} else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {62			unprefixed.as_bytes().to_owned()63		} else {64			let secret_prefix = format!("{SECRET_PREFIX}{Z85_ENCODED_PREFIX}");65			return Err(format!(66				"unknown secret encoding. If you're migrating from old version of fleet, prefix public secret fields with {PLAINTEXT_PREFIX:?}, and encrypted data with {secret_prefix:?}: {string}"67			));68		};69		Ok(Self { data, encrypted })70	}71}7273impl Display for SecretData {74	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {75		let mut readable = std::str::from_utf8(&self.data).ok();76		if self.encrypted {77			write!(f, "{SECRET_PREFIX}")?;78			// Always base64-encode encrypted fields.79			readable = None;80		}81		if Some(false) == readable.map(is_printable) {82			readable = None83		};84		// TODO: Check if text is readable, and has no unprintable characters?..85		if let Some(plaintext) = readable {86			if plaintext.ends_with('\n') {87				write!(f, "{PLAINTEXT_NEWLINE_PREFIX}")?;88			} else {89				write!(f, "{PLAINTEXT_PREFIX}")?;90			}91			write!(f, "{plaintext}")?;92		} else {93			write!(f, "{BASE64_ENCODED_PREFIX}")?;94			let encoded = STANDARD_NO_PAD.encode(&self.data);95			for ele in encoded.as_bytes().chunks(64) {96				let chunk = std::str::from_utf8(ele).expect(97					"any slice of base64-encoded text is utf-8 compatible, as it is ascii-based",98				);99				writeln!(f, "{chunk}")?;100			}101		};102		Ok(())103	}104}105106fn is_printable(text: &str) -> bool {107	text.chars().all(|c| {108		c.is_letter()109			|| c.is_mark()110			|| c.is_number()111			|| c.is_punctuation()112			|| c.is_separator()113			|| c == '\n' || c == '\t'114			// Complete base64 alphabet115			|| c == '/' || c == '+'116			|| c == '='117	})118}119120#[test]121fn test() {122	fn check_roundtrip(data: SecretData, expected: &str) {123		let string = data.to_string();124		assert_eq!(string, expected, "unexpected encoding");125		let roundtrip: SecretData = string.parse().expect("roundtrip parse");126		assert_eq!(data, roundtrip, "roundtrip didn't match");127	}128	check_roundtrip(129		SecretData {130			data: vec![1, 2, 3, 4, 5, 6],131			encrypted: false,132		},133		"<BASE64-ENCODED>\nAQIDBAUG\n",134	);135	check_roundtrip(136		SecretData {137			data: vec![1, 2, 3, 4, 5, 6],138			encrypted: true,139		},140		"<ENCRYPTED><BASE64-ENCODED>\nAQIDBAUG\n",141	);142	check_roundtrip(143		SecretData {144			data: "Привет, мир!\n".to_owned().into(),145			encrypted: false,146		},147		"<PLAINTEXT-NL>\nПривет, мир!\n",148	);149	check_roundtrip(150		SecretData {151			data: "Привет, мир!".to_owned().into(),152			encrypted: false,153		},154		"<PLAINTEXT>Привет, мир!",155	);156}
after · crates/fleet-shared/src/encoding.rs
1use std::{2	collections::BTreeMap, fmt::{self, Display}, str::FromStr3};45use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};6use serde::{Deserialize, Deserializer, Serialize, de::Error};7use unicode_categories::UnicodeCategories;89#[derive(Debug, PartialEq, Clone)]10pub struct SecretData {11	pub data: Vec<u8>,12	pub encrypted: bool,13}1415const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";16const Z85_ENCODED_PREFIX: &str = "<Z85-ENCODED>\n";17// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.18const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";19const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";2021const SECRET_PREFIX: &str = "<ENCRYPTED>";2223impl<'de> Deserialize<'de> for SecretData {24	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>25	where26		D: Deserializer<'de>,27	{28		let string = String::deserialize(deserializer)?;29		string.parse().map_err(D::Error::custom)30	}31}3233impl Serialize for SecretData {34	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>35	where36		S: serde::Serializer,37	{38		self.to_string().serialize(serializer)39	}40}4142impl FromStr for SecretData {43	type Err = String;4445	fn from_str(string: &str) -> Result<Self, Self::Err> {46		let (encrypted, string) = if let Some(unprefixed) = string.strip_prefix(SECRET_PREFIX) {47			(true, unprefixed)48		} else {49			(false, string)50		};51		let data = if let Some(unprefixed) = string.strip_prefix(BASE64_ENCODED_PREFIX) {52			STANDARD_NO_PAD53				.decode(unprefixed.replace(['\n', '\t', ' '], ""))54				.map_err(|e| format!("base64-encoded failed: {e}"))?55		} else if let Some(unprefixed) = string.strip_prefix(Z85_ENCODED_PREFIX) {56			z85::decode(unprefixed.replace(['\n', '\t', ' '], ""))57				.map_err(|e| format!("z85-encoded failed: {e}"))?58		} else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {59			unprefixed.as_bytes().to_owned()60		} else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {61			unprefixed.as_bytes().to_owned()62		} else {63			let secret_prefix = format!("{SECRET_PREFIX}{Z85_ENCODED_PREFIX}");64			return Err(format!(65				"unknown secret encoding. If you're migrating from old version of fleet, prefix public secret fields with {PLAINTEXT_PREFIX:?}, and encrypted data with {secret_prefix:?}: {string}"66			));67		};68		Ok(Self { data, encrypted })69	}70}7172impl Display for SecretData {73	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {74		let mut readable = std::str::from_utf8(&self.data).ok();75		if self.encrypted {76			write!(f, "{SECRET_PREFIX}")?;77			// Always base64-encode encrypted fields.78			readable = None;79		}80		if Some(false) == readable.map(is_printable) {81			readable = None82		};83		// TODO: Check if text is readable, and has no unprintable characters?..84		if let Some(plaintext) = readable {85			if plaintext.ends_with('\n') {86				write!(f, "{PLAINTEXT_NEWLINE_PREFIX}")?;87			} else {88				write!(f, "{PLAINTEXT_PREFIX}")?;89			}90			write!(f, "{plaintext}")?;91		} else {92			write!(f, "{BASE64_ENCODED_PREFIX}")?;93			let encoded = STANDARD_NO_PAD.encode(&self.data);94			for ele in encoded.as_bytes().chunks(64) {95				let chunk = std::str::from_utf8(ele).expect(96					"any slice of base64-encoded text is utf-8 compatible, as it is ascii-based",97				);98				writeln!(f, "{chunk}")?;99			}100		};101		Ok(())102	}103}104105fn is_printable(text: &str) -> bool {106	text.chars().all(|c| {107		c.is_letter()108			|| c.is_mark()109			|| c.is_number()110			|| c.is_punctuation()111			|| c.is_separator()112			|| c == '\n' || c == '\t'113			// Complete base64 alphabet114			|| c == '/' || c == '+'115			|| c == '='116	})117}118119#[test]120fn test() {121	fn check_roundtrip(data: SecretData, expected: &str) {122		let string = data.to_string();123		assert_eq!(string, expected, "unexpected encoding");124		let roundtrip: SecretData = string.parse().expect("roundtrip parse");125		assert_eq!(data, roundtrip, "roundtrip didn't match");126	}127	check_roundtrip(128		SecretData {129			data: vec![1, 2, 3, 4, 5, 6],130			encrypted: false,131		},132		"<BASE64-ENCODED>\nAQIDBAUG\n",133	);134	check_roundtrip(135		SecretData {136			data: vec![1, 2, 3, 4, 5, 6],137			encrypted: true,138		},139		"<ENCRYPTED><BASE64-ENCODED>\nAQIDBAUG\n",140	);141	check_roundtrip(142		SecretData {143			data: "Привет, мир!\n".to_owned().into(),144			encrypted: false,145		},146		"<PLAINTEXT-NL>\nПривет, мир!\n",147	);148	check_roundtrip(149		SecretData {150			data: "Привет, мир!".to_owned().into(),151			encrypted: false,152		},153		"<PLAINTEXT>Привет, мир!",154	);155}
modifiedflake.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