difftreelog
feat minimal rollback support
in: trunk
20 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -83,10 +83,10 @@
"i18n-embed",
"i18n-embed-fl",
"lazy_static",
- "nom",
+ "nom 7.1.3",
"num-traits",
"pin-project",
- "rand",
+ "rand 0.8.5",
"rsa",
"rust-embed",
"scrypt",
@@ -107,8 +107,8 @@
"cookie-factory",
"hkdf",
"io_tee",
- "nom",
- "rand",
+ "nom 7.1.3",
+ "rand 0.8.5",
"secrecy",
"sha2",
]
@@ -233,18 +233,18 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
name = "async-trait"
-version = "0.1.83"
+version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -395,15 +395,15 @@
"regex",
"rustc-hash",
"shlex",
- "syn 2.0.87",
+ "syn",
"which",
]
[[package]]
name = "bitflags"
-version = "2.6.0"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
dependencies = [
"serde",
]
@@ -493,7 +493,7 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
- "nom",
+ "nom 7.1.3",
]
[[package]]
@@ -534,9 +534,9 @@
[[package]]
name = "chrono"
-version = "0.4.38"
+version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -544,7 +544,7 @@
"num-traits",
"serde",
"wasm-bindgen",
- "windows-targets",
+ "windows-link",
]
[[package]]
@@ -609,10 +609,10 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
- "heck 0.5.0",
+ "heck",
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -647,6 +647,15 @@
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
+name = "convert_case"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
name = "cookie-factory"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -684,16 +693,18 @@
[[package]]
name = "crossterm"
-version = "0.28.1"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags",
"crossterm_winapi",
+ "derive_more",
+ "document-features",
"filedescriptor",
"mio",
"parking_lot",
- "rustix",
+ "rustix 1.0.7",
"signal-hook",
"signal-hook-mio",
"winapi",
@@ -715,7 +726,7 @@
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
- "rand_core",
+ "rand_core 0.6.4",
"typenum",
]
@@ -752,7 +763,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -781,15 +792,36 @@
[[package]]
name = "deranged"
-version = "0.3.11"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -809,10 +841,19 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
+name = "document-features"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -830,7 +871,6 @@
dependencies = [
"curve25519-dalek",
"ed25519",
- "rand_core",
"serde",
"sha2",
"subtle",
@@ -857,12 +897,12 @@
[[package]]
name = "errno"
-version = "0.3.9"
+version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -924,10 +964,10 @@
"hostname",
"human-repr",
"indicatif",
- "itertools 0.13.0",
+ "itertools 0.14.0",
"nix-eval",
"nixlike",
- "nom",
+ "nom 8.0.0",
"openssh",
"owo-colors",
"peg",
@@ -958,15 +998,17 @@
"futures",
"hostname",
"indoc",
- "itertools 0.13.0",
+ "itertools 0.14.0",
"nix-eval",
"nixlike",
- "nom",
+ "nom 8.0.0",
"openssh",
- "rand",
+ "rand 0.9.1",
"serde",
"serde_json",
+ "tabled",
"tempfile",
+ "time",
"tokio",
"tokio-util",
"tracing",
@@ -983,7 +1025,7 @@
"ed25519-dalek",
"fleet-shared",
"hex",
- "rand",
+ "rand 0.9.1",
"x25519-dalek",
]
@@ -1119,7 +1161,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -1170,7 +1212,19 @@
dependencies = [
"cfg-if",
"libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
]
[[package]]
@@ -1237,12 +1291,6 @@
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
-
-[[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
@@ -1297,13 +1345,13 @@
[[package]]
name = "hostname"
-version = "0.4.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
+checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
dependencies = [
"cfg-if",
"libc",
- "windows",
+ "windows-link",
]
[[package]]
@@ -1463,7 +1511,7 @@
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.87",
+ "syn",
"unic-langid",
]
@@ -1477,7 +1525,7 @@
"i18n-config",
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -1620,6 +1668,15 @@
]
[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1651,9 +1708,9 @@
[[package]]
name = "libc"
-version = "0.2.164"
+version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libloading"
@@ -1694,6 +1751,18 @@
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litrs"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+
+[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1778,7 +1847,7 @@
"hermit-abi 0.3.9",
"libc",
"log",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -1790,9 +1859,9 @@
[[package]]
name = "nix"
-version = "0.29.0"
+version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
@@ -1807,13 +1876,13 @@
"anyhow",
"better-command",
"futures",
- "itertools 0.13.0",
+ "itertools 0.14.0",
"nixlike",
"r2d2",
"regex",
"serde",
"serde_json",
- "thiserror 2.0.3",
+ "thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
@@ -1839,7 +1908,7 @@
"serde",
"serde-transcode",
"serde_json",
- "thiserror 2.0.3",
+ "thiserror 2.0.12",
]
[[package]]
@@ -1872,6 +1941,15 @@
]
[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1893,7 +1971,7 @@
"num-integer",
"num-iter",
"num-traits",
- "rand",
+ "rand 0.8.5",
"smallvec",
"zeroize",
]
@@ -1963,15 +2041,15 @@
[[package]]
name = "openssh"
-version = "0.11.3"
+version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b52987a10526b8daef7f1946b0aadfc214479f897ba624776327fd3beec2722c"
+checksum = "ea0bb128ba90e86bc55dae66031935f361cda4cbc1f011547c55a7d80079bc3e"
dependencies = [
"libc",
"once_cell",
"shell-escape",
"tempfile",
- "thiserror 2.0.3",
+ "thiserror 2.0.12",
"tokio",
]
@@ -1983,9 +2061,9 @@
[[package]]
name = "owo-colors"
-version = "4.1.0"
+version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
+checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec"
dependencies = [
"supports-color 2.1.0",
"supports-color 3.0.1",
@@ -1993,13 +2071,13 @@
[[package]]
name = "papergrid"
-version = "0.12.0"
+version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725"
+checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1"
dependencies = [
"bytecount",
"fnv",
- "unicode-width 0.1.11",
+ "unicode-width 0.2.0",
]
[[package]]
@@ -2043,9 +2121,9 @@
[[package]]
name = "peg"
-version = "0.8.4"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f"
+checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477"
dependencies = [
"peg-macros",
"peg-runtime",
@@ -2053,9 +2131,9 @@
[[package]]
name = "peg-macros"
-version = "0.8.4"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426"
+checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71"
dependencies = [
"peg-runtime",
"proc-macro2",
@@ -2064,9 +2142,9 @@
[[package]]
name = "peg-runtime"
-version = "0.8.3"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
+checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca"
[[package]]
name = "pem"
@@ -2111,7 +2189,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -2204,31 +2282,7 @@
checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
dependencies = [
"proc-macro2",
- "syn 2.0.87",
-]
-
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
+ "syn",
]
[[package]]
@@ -2250,7 +2304,7 @@
"proc-macro-error-attr2",
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -2279,7 +2333,7 @@
checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15"
dependencies = [
"bytes",
- "heck 0.5.0",
+ "heck",
"itertools 0.13.0",
"log",
"multimap",
@@ -2289,7 +2343,7 @@
"prost",
"prost-types",
"regex",
- "syn 2.0.87",
+ "syn",
"tempfile",
]
@@ -2303,7 +2357,7 @@
"itertools 0.13.0",
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -2325,6 +2379,12 @@
]
[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
name = "r2d2"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2342,18 +2402,38 @@
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
]
[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
]
[[package]]
@@ -2362,10 +2442,19 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
]
[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
name = "rcgen"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2439,7 +2528,7 @@
dependencies = [
"cc",
"cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
"libc",
"spin",
"untrusted",
@@ -2481,14 +2570,15 @@
[[package]]
name = "ron"
-version = "0.8.1"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f"
dependencies = [
- "base64 0.21.7",
+ "base64 0.22.1",
"bitflags",
"serde",
"serde_derive",
+ "unicode-ident",
]
[[package]]
@@ -2517,7 +2607,7 @@
"num-traits",
"pkcs1",
"pkcs8",
- "rand_core",
+ "rand_core 0.6.4",
"signature",
"spki",
"subtle",
@@ -2544,7 +2634,7 @@
"proc-macro2",
"quote",
"rust-embed-utils",
- "syn 2.0.87",
+ "syn",
"walkdir",
]
@@ -2588,11 +2678,24 @@
"bitflags",
"errno",
"libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.14",
"windows-sys 0.52.0",
]
[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.4",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "rustls"
version = "0.23.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2721,9 +2824,9 @@
[[package]]
name = "serde"
-version = "1.0.215"
+version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
@@ -2748,20 +2851,20 @@
[[package]]
name = "serde_derive"
-version = "1.0.215"
+version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
name = "serde_json"
-version = "1.0.133"
+version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
@@ -2838,7 +2941,7 @@
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
- "rand_core",
+ "rand_core 0.6.4",
]
[[package]]
@@ -2920,17 +3023,6 @@
checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77"
dependencies = [
"is_ci",
-]
-
-[[package]]
-name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
]
[[package]]
@@ -2958,37 +3050,38 @@
[[package]]
name = "tabled"
-version = "0.16.0"
+version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b"
+checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d"
dependencies = [
"papergrid",
"tabled_derive",
+ "testing_table",
]
[[package]]
name = "tabled_derive"
-version = "0.8.0"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069"
+checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846"
dependencies = [
- "heck 0.4.1",
- "proc-macro-error",
+ "heck",
+ "proc-macro-error2",
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn",
]
[[package]]
name = "tempfile"
-version = "3.14.0"
+version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
- "cfg-if",
"fastrand",
+ "getrandom 0.3.3",
"once_cell",
- "rustix",
+ "rustix 1.0.7",
"windows-sys 0.59.0",
]
@@ -2998,7 +3091,7 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef"
dependencies = [
- "rustix",
+ "rustix 0.38.40",
"windows-sys 0.59.0",
]
@@ -3014,6 +3107,15 @@
]
[[package]]
+name = "testing_table"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc"
+dependencies = [
+ "unicode-width 0.2.0",
+]
+
+[[package]]
name = "text-size"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3058,11 +3160,11 @@
[[package]]
name = "thiserror"
-version = "2.0.3"
+version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
- "thiserror-impl 2.0.3",
+ "thiserror-impl 2.0.12",
]
[[package]]
@@ -3073,18 +3175,18 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
name = "thiserror-impl"
-version = "2.0.3"
+version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -3099,9 +3201,9 @@
[[package]]
name = "time"
-version = "0.3.36"
+version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"num-conv",
@@ -3113,15 +3215,15 @@
[[package]]
name = "time-core"
-version = "0.1.2"
+version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
-version = "0.2.18"
+version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
@@ -3138,9 +3240,9 @@
[[package]]
name = "tokio"
-version = "1.41.1"
+version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [
"backtrace",
"bytes",
@@ -3155,13 +3257,13 @@
[[package]]
name = "tokio-macros"
-version = "2.4.0"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -3189,9 +3291,9 @@
[[package]]
name = "tokio-util"
-version = "0.7.12"
+version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
dependencies = [
"bytes",
"futures-core",
@@ -3252,7 +3354,7 @@
"prost-build",
"prost-types",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -3266,7 +3368,7 @@
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
- "rand",
+ "rand 0.8.5",
"slab",
"tokio",
"tokio-util",
@@ -3337,7 +3439,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -3457,6 +3559,12 @@
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3476,9 +3584,9 @@
[[package]]
name = "unindent"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]]
name = "universal-hash"
@@ -3573,6 +3681,15 @@
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
name = "wasm-bindgen"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3594,7 +3711,7 @@
"once_cell",
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
"wasm-bindgen-shared",
]
@@ -3616,7 +3733,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3646,7 +3763,7 @@
"either",
"home",
"once_cell",
- "rustix",
+ "rustix 0.38.40",
]
[[package]]
@@ -3681,23 +3798,19 @@
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "windows"
+name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
- "windows-core",
"windows-targets",
]
[[package]]
-name = "windows-core"
-version = "0.52.0"
+name = "windows-link"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
-dependencies = [
- "windows-targets",
-]
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-sys"
@@ -3782,13 +3895,22 @@
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
name = "x25519-dalek"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek",
- "rand_core",
+ "rand_core 0.6.4",
"serde",
"zeroize",
]
@@ -3804,9 +3926,9 @@
[[package]]
name = "z85"
-version = "3.0.5"
+version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a599daf1b507819c1121f0bf87fa37eb19daac6aff3aefefd4e6e2e0f2020fc"
+checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64"
[[package]]
name = "zerocopy"
@@ -3826,7 +3948,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
[[package]]
@@ -3846,5 +3968,5 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.87",
+ "syn",
]
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,12 +15,12 @@
anyhow = "1.0"
clap = { version = "4.5", features = ["derive", "env", "unicode", "wrap_help"] }
clap_complete = "4.5"
-nix = { version = "0.29.0", features = ["fs", "user"] }
+nix = { version = "0.30.1", features = ["fs", "user"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-tempfile = "3.10"
-thiserror = "2.0.3"
-tokio = { version = "1.36.0", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
-tokio-util = { version = "0.7.11", features = ["codec"] }
+tempfile = "3.20"
+thiserror = "2.0.12"
+tokio = { version = "1.45.1", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] }
+tokio-util = { version = "0.7.15", features = ["codec"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
cmds/fleet/Cargo.tomldiffbeforeafterboth--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -5,6 +5,7 @@
authors = ["Yaroslav Bolyukin <iam@lach.pw>"]
edition.workspace = true
rust-version.workspace = true
+default-run = "fleet"
[dependencies]
age = { workspace = true, features = ["armor"] }
@@ -27,23 +28,23 @@
async-trait = "0.1"
base64 = "0.22.1"
chrono = { version = "0.4", features = ["serde"] }
-crossterm = { version = "0.28.0", features = ["use-dev-tty"] }
+crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
futures = "0.3"
-hostname = "0.4.0"
-itertools = "0.13"
+hostname = "0.4.1"
+itertools = "0.14"
openssh = "0.11"
-owo-colors = { version = "4.0", features = ["supports-color", "supports-colors"] }
+owo-colors = { version = "4.2", features = ["supports-color", "supports-colors"] }
peg = "0.8"
-regex = "1.10"
+regex = "1.11"
shlex = "1.3"
-tabled = { version = "0.16" }
+tabled = { version = "0.20" }
time = { version = "0.3", features = ["serde"] }
tokio-util = { version = "0.7", features = ["codec"] }
fleet-base = { version = "0.1.0", path = "../../crates/fleet-base" }
human-repr = { version = "1.1", optional = true }
indicatif = { version = "0.17", optional = true }
-nom = "7.1.3"
+nom = "8.0.0"
tracing-indicatif = { version = "0.3", optional = true }
[features]
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -1,14 +1,14 @@
-use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf, time::Duration};
+use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};
-use anyhow::{anyhow, bail, Context, Result};
-use clap::{Parser, ValueEnum};
+use anyhow::{anyhow, Result};
+use clap::Parser;
use fleet_base::{
- host::{Config, ConfigHost, DeployKind},
+ deploy::{deploy_task, upload_task, DeployAction},
+ host::{Config, DeployKind, GenerationStorage},
opts::FleetOpts,
};
-use itertools::Itertools as _;
use nix_eval::{nix_go, NixBuildBatch};
-use tokio::{task::LocalSet, time::sleep};
+use tokio::task::LocalSet;
use tracing::{error, field, info, info_span, warn, Instrument};
#[derive(Parser)]
@@ -18,300 +18,16 @@
disable_rollback: bool,
/// Action to execute after system is built
action: DeployAction,
-}
-
-#[derive(ValueEnum, Clone, Copy)]
-enum DeployAction {
- /// Upload derivation, but do not execute the update.
- Upload,
- /// Upload and execute the activation script, old version will be used after reboot.
- Test,
- /// Upload and set as current system profile, but do not execute activation script.
- Boot,
- /// Upload, set current profile, and execute activation script.
- Switch,
}
-impl DeployAction {
- pub(crate) fn name(&self) -> Option<&'static str> {
- match self {
- Self::Upload => None,
- Self::Test => Some("test"),
- Self::Boot => Some("boot"),
- Self::Switch => Some("switch"),
- }
- }
- pub(crate) fn should_switch_profile(&self) -> bool {
- matches!(self, Self::Switch | Self::Boot)
- }
- pub(crate) fn should_activate(&self) -> bool {
- matches!(self, Self::Switch | Self::Test | Self::Boot)
- }
- pub(crate) fn should_create_rollback_marker(&self) -> bool {
- // Upload does nothing on the target machine, other than uploading the closure.
- // In boot case we want to have rollback marker prepared, so that the system may rollback itself on the next boot.
- !matches!(self, Self::Upload)
- }
- pub(crate) fn should_schedule_rollback_run(&self) -> bool {
- matches!(self, Self::Switch | Self::Test)
- }
-}
-
#[derive(Parser, Clone)]
pub struct BuildSystems {
/// Attribute to build. Systems are deployed from "toplevel" attr, well-known used attributes
/// are "sdImage"/"isoImage", and your configuration may include any other build attributes.
#[clap(long, default_value = "toplevel")]
build_attr: String,
-}
-
-struct Generation {
- id: u32,
- current: bool,
- datetime: String,
-}
-
-fn parse_generation_line(g: &str) -> Option<Generation> {
- let mut parts = g.split_whitespace();
- let id = parts.next()?;
- let id: u32 = id.parse().ok()?;
- let date = parts.next()?;
- let time = parts.next()?;
- let current = if let Some(current) = parts.next() {
- if current == "(current)" {
- Some(true)
- } else {
- None
- }
- } else {
- Some(false)
- };
- let current = current?;
- if parts.next().is_some() {
- warn!("unexpected text after generation: {g}");
- }
- Some(Generation {
- id,
- current,
- datetime: format!("{date} {time}"),
- })
-}
-
-async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {
- let mut cmd = host.cmd("nix-env").await?;
- cmd.comparg("--profile", "/nix/var/nix/profiles/system")
- .arg("--list-generations");
- // Sudo is required due to --list-generations acquiring lock on the profile.
- let data = cmd.sudo().run_string().await?;
- let generations = data
- .split('\n')
- .map(|e| e.trim())
- .filter(|&l| !l.is_empty())
- .filter_map(|g| {
- let gen = parse_generation_line(g);
- if gen.is_none() {
- warn!("bad generation: {g}");
- }
- gen
- })
- .collect::<Vec<_>>();
- let current = generations
- .into_iter()
- .filter(|g| g.current)
- .at_most_one()
- .map_err(|_e| anyhow!("bad list-generations output"))?
- .ok_or_else(|| anyhow!("failed to find generation"))?;
- Ok(current)
}
-
-async fn deploy_task(
- action: DeployAction,
- host: &ConfigHost,
- built: PathBuf,
- specialisation: Option<String>,
- disable_rollback: bool,
-) -> Result<()> {
- let deploy_kind = host.deploy_kind().await?;
- if (deploy_kind == DeployKind::NixosInstall || deploy_kind == DeployKind::NixosLustrate)
- && !matches!(action, DeployAction::Boot | DeployAction::Upload)
- {
- bail!("{deploy_kind:?} deploy kind only supports boot and upload actions");
- }
-
- let mut failed = false;
- // TODO: Lockfile, to prevent concurrent system switch?
- // TODO: If rollback target exists - bail, it should be removed. Lockfile will not work in case if rollback
- // is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to
- // unit name conflict in systemd-run
- // This code is tied to rollback.nix
- if !disable_rollback && action.should_create_rollback_marker() {
- let _span = info_span!("preparing").entered();
- info!("preparing for rollback");
- let generation = get_current_generation(host).await?;
- info!(
- "rollback target would be {} {}",
- generation.id, generation.datetime
- );
- {
- let mut cmd = host.cmd("sh").await?;
- cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));
- if let Err(e) = cmd.sudo().run().await {
- error!("failed to set rollback marker: {e}");
- failed = true;
- }
- }
- // Activation script also starts rollback-watchdog.timer, however, it is possible that it won't be started.
- // Kicking it on manually will work best.
- //
- // There wouldn't be conflict, because here we trigger start of the primary service, and systemd will
- // only allow one instance of it.
-
- // TODO: We should also watch how this process is going.
- // After running this command, we have less than 3 minutes to deploy everything,
- // if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.
- // Anyway, reboot will still help in this case.
- if action.should_schedule_rollback_run() {
- let mut cmd = host.cmd("systemd-run").await?;
- cmd.comparg("--on-active", "3min")
- .comparg("--unit", "rollback-watchdog-run")
- .arg("systemctl")
- .arg("start")
- .arg("rollback-watchdog.service");
- if let Err(e) = cmd.sudo().run().await {
- error!("failed to schedule rollback run: {e}");
- failed = true;
- }
- }
- }
- if deploy_kind == DeployKind::NixosLustrate {
- // Fleet could also create this file, but as this operation is potentially disruptive,
- // make user do it themself.
- if !host.file_exists("/etc/NIXOS_LUSTRATE").await? {
- bail!("/etc/NIXOS_LUSTRATE should be created on remote host");
- }
- // Wanted by NixOS to recognize the system as NixOS.
- let mut cmd = host.cmd("touch").await?;
- cmd.arg("/etc/NIXOS");
- cmd.sudo().run().await.context("creating /etc/NIXOS")?;
- }
- if deploy_kind == DeployKind::NixosInstall {
- info!(
- "running nixos-install to switch profile, install bootloader, and perform activation"
- );
- let mut cmd = host.cmd("nixos-install").await?;
- cmd.arg("--system").arg(&built).args([
- // Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
- // It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
- "--no-channel-copy",
- "--root",
- "/mnt",
- ]);
- if let Err(e) = cmd.sudo().run().await {
- error!("failed to execute nixos-install: {e}");
- failed = true;
- }
- } else {
- if action.should_switch_profile() && !failed {
- info!("switching system profile generation");
-
- // To avoid even more problems, using nixos-install for now.
- // // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
- // // falling back to using nix-env command
- // // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
- // let mut cmd = host.cmd("nix-env").await?;
- // cmd.args([
- // "--store",
- // "/mnt",
- // "--profile",
- // "/mnt/nix/var/nix/profiles/system",
- // "--set",
- // ])
- // .arg(&built);
- // if let Err(e) = cmd.sudo().run_nix().await {
- // error!("failed to switch system profile generation: {e}");
- // failed = true;
- // }
- // It would also be possible to update profile atomically during copy:
- // https://github.com/NixOS/nix/pull/11657
- let mut cmd = host.nix_cmd().await?;
- cmd.arg("build");
- cmd.comparg("--profile", "/nix/var/nix/profiles/system");
- cmd.arg(&built);
- if let Err(e) = cmd.sudo().run_nix().await {
- error!("failed to switch system profile generation: {e}");
- failed = true;
- }
- }
-
- // FIXME: Connection might be disconnected after activation run
-
- if action.should_activate() && !failed {
- let _span = info_span!("activating").entered();
- info!("executing activation script");
- let specialised = if let Some(specialisation) = specialisation {
- let mut specialised = built.join("specialisation");
- specialised.push(specialisation);
- specialised
- } else {
- built.clone()
- };
- let switch_script = specialised.join("bin/switch-to-configuration");
- let mut cmd = host.cmd(switch_script).in_current_span().await?;
- if deploy_kind == DeployKind::NixosLustrate {
- cmd.env("NIXOS_INSTALL_BOOTLOADER", "1");
- }
- cmd.env("FLEET_ONLINE_ACTIVATION", "1")
- .arg(action.name().expect("upload.should_activate == false"));
- if let Err(e) = cmd.sudo().run().in_current_span().await {
- error!("failed to activate: {e}");
- failed = true;
- }
- }
- }
- if action.should_create_rollback_marker() {
- if !disable_rollback {
- if failed {
- if action.should_schedule_rollback_run() {
- info!("executing rollback");
- if let Err(e) = host
- .systemctl_start("rollback-watchdog.service")
- .instrument(info_span!("rollback"))
- .await
- {
- error!("failed to trigger rollback: {e}")
- }
- }
- } else {
- info!("trying to mark upgrade as successful");
- if let Err(e) = host
- .rm_file("/etc/fleet_rollback_marker", true)
- .in_current_span()
- .await
- {
- 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");
- if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {
- // It is ok, if there was no reboot - then timer might not be running.
- }
- if action.should_schedule_rollback_run() {
- if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {
- 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.
- }
- }
- Ok(())
-}
-
async fn build_task(
config: Config,
hostname: String,
@@ -328,7 +44,8 @@
.get("out")
.ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;
- {
+ // We already have system profiles for backups.
+ if !host.local {
info!("adding gc root");
let mut cmd = config.local_host().cmd("nix").await?;
cmd.arg("build")
@@ -403,7 +120,6 @@
let config = config.clone();
let span = info_span!("deploy", host = field::display(&host.name));
let hostname = host.name.clone();
- let local_host = config.local_host();
let opts = opts.clone();
let batch = batch.clone();
if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
@@ -437,51 +153,20 @@
disable_rollback = true;
}
- if !opts.is_local(&hostname) {
- info!("uploading system closure");
+ let remote_path =
+ match upload_task(&config, &host, GenerationStorage::Deployer, built).await
{
- // TODO: Move to remote_derivation method.
- // Alternatively, nix store make-content-addressed can be used,
- // at least for the first deployment, to provide trusted store key.
- //
- // It is much slower, yet doesn't require root on the deployer machine.
- let Ok(mut sign) = local_host.cmd("nix").await else {
- error!("failed to setup local");
+ Ok(v) => v,
+ Err(e) => {
+ error!("upload failed: {e}");
return;
- };
- // Private key for host machine is registered in nix-sign.nix
- sign.arg("store")
- .arg("sign")
- .comparg("--key-file", "/etc/nix/private-key")
- .arg("-r")
- .arg(&built);
- if let Err(e) = sign.sudo().run_nix().await {
- warn!("failed to sign store paths: {e}");
- };
- }
- let mut tries = 0;
- loop {
- match host.remote_derivation(&built).await {
- Ok(remote) => {
- assert!(remote == built, "CA derivations aren't implemented");
- break;
- }
- Err(e) if tries < 3 => {
- tries += 1;
- warn!("copy failure ({}/3): {}", tries, e);
- sleep(Duration::from_millis(5000)).await;
- }
- Err(e) => {
- error!("upload failed: {e}");
- return;
- }
}
- }
- }
+ };
+
if let Err(e) = deploy_task(
self.action,
&host,
- built,
+ remote_path,
if let Ok(v) = opts.action_attr(&host, "specialisation").await {
v
} else {
cmds/fleet/src/cmds/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/mod.rs
+++ b/cmds/fleet/src/cmds/mod.rs
@@ -3,3 +3,4 @@
pub mod info;
pub mod secrets;
pub mod tf;
+pub mod rollback;
\ No newline at end of file
cmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -0,0 +1,127 @@
+use std::collections::HashSet;
+
+use anyhow::{bail, Result};
+use clap::Parser;
+use fleet_base::{
+ deploy::{deploy_task, upload_task, DeployAction},
+ host::{Config, ConfigHost, Generation, GenerationStorage},
+ opts::FleetOpts,
+};
+use tabled::Table;
+use tracing::{info, warn};
+
+#[derive(Parser)]
+pub struct RollbackSingle {
+ machine: String,
+ #[clap(subcommand)]
+ action: RollbackAction,
+}
+
+#[derive(Parser, Clone)]
+struct DeployOptions {
+ /// Rollback target to use
+ id: String,
+ /// Rollback to the current generation if rollback fails
+ // Automatic rollback seems to be unnecessary for manual rollback...
+ #[clap(long)]
+ enable_rollback: bool,
+ /// Specialization to use
+ #[clap(long)]
+ specialization: Option<String>,
+}
+
+#[derive(Parser, Clone)]
+enum RollbackAction {
+ /// List available rollback targets
+ ListTargets,
+ /// Upload and execute the activation script, old version will be used after reboot.
+ Test(#[clap(flatten)] DeployOptions),
+ /// Upload, set current profile, and execute activation script.
+ Switch(#[clap(flatten)] DeployOptions),
+ /// Upload and set as current system profile, but do not execute activation script.
+ Boot(#[clap(flatten)] DeployOptions),
+}
+
+pub async fn list_all_generations(host: &ConfigHost, config: &Config) -> Vec<Generation> {
+ let stored_on_machine = host
+ .list_generations("system")
+ .await
+ .inspect_err(|e| {
+ warn!("failed to list generations available on the remote machine: {e}");
+ })
+ .unwrap_or_default();
+ let on_machine_store_paths = stored_on_machine
+ .iter()
+ .map(|g| &g.store_path)
+ .collect::<HashSet<_>>();
+ let mut stored_locally = config
+ .local_host()
+ .list_generations(&format!("{}-{}", config.data().gc_root_prefix, host.name))
+ .await
+ .inspect_err(|e| {
+ warn!("failed to list generations available locally: {e}");
+ })
+ .unwrap_or_default();
+ dbg!(&stored_locally);
+ stored_locally.retain(|g| !on_machine_store_paths.contains(&g.store_path));
+ for ele in stored_locally.iter_mut() {
+ ele.current = false;
+ ele.location = GenerationStorage::Deployer;
+ }
+ stored_locally.extend(stored_on_machine);
+ stored_locally.sort_by_key(|v| v.datetime);
+ stored_locally
+}
+
+impl RollbackSingle {
+ pub(crate) async fn run(&self, config: &Config, _opts: &FleetOpts) -> Result<()> {
+ let host = config.host(&self.machine).await?;
+ match &self.action {
+ RollbackAction::ListTargets => {
+ let generations = list_all_generations(&host, config).await;
+ if generations.is_empty() {
+ bail!("no available rollback targets found");
+ }
+ info!("Generation list:\n{}", Table::new(&generations));
+ Ok(())
+ }
+ RollbackAction::Boot(o) | RollbackAction::Test(o) | RollbackAction::Switch(o) => {
+ let DeployOptions {
+ id,
+ enable_rollback,
+ specialization,
+ } = o;
+ let action: DeployAction = match self.action {
+ RollbackAction::Test { .. } => DeployAction::Test,
+ RollbackAction::Switch { .. } => DeployAction::Switch,
+ RollbackAction::Boot { .. } => DeployAction::Boot,
+ _ => unreachable!(),
+ };
+ let generations = list_all_generations(&host, config).await;
+ let Some(generation) = generations.iter().find(|g| &g.rollback_id() == id) else {
+ bail!(
+ "generation by this name is not found, existing generations:\n{}",
+ Table::new(&generations)
+ );
+ };
+ let remote_path = upload_task(
+ config,
+ &host,
+ generation.location,
+ generation.store_path.clone(),
+ )
+ .await?;
+
+ deploy_task(
+ action,
+ &host,
+ remote_path,
+ specialization.clone(),
+ !*enable_rollback,
+ )
+ .await?;
+ Ok(())
+ }
+ }
+ }
+}
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -10,6 +10,7 @@
use clap::{CommandFactory, Parser};
use cmds::{
build_systems::{BuildSystems, Deploy},
+ rollback::RollbackSingle,
complete::Complete,
info::Info,
secrets::Secret,
@@ -70,6 +71,8 @@
BuildSystems(BuildSystems),
/// Upload and switch system closures
Deploy(Deploy),
+ /// Rollback remote machine by redeploying old generation as the new one
+ RollbackSingle(RollbackSingle),
/// Secret management
#[clap(subcommand)]
Secret(Secret),
@@ -97,6 +100,7 @@
match command {
Opts::BuildSystems(c) => c.run(config, &opts).await?,
Opts::Deploy(d) => d.run(config, &opts).await?,
+ Opts::RollbackSingle(r) => r.run(config, &opts).await?,
Opts::Secret(s) => s.run(config, &opts).await?,
Opts::Info(i) => i.run(config).await?,
Opts::Prefetch(p) => p.run(config).await?,
cmds/generator-helper/Cargo.tomldiffbeforeafterboth--- a/cmds/generator-helper/Cargo.toml
+++ b/cmds/generator-helper/Cargo.toml
@@ -11,7 +11,7 @@
fleet-shared.workspace = true
base64 = "0.22.1"
-ed25519-dalek = { version = "2.1", features = ["rand_core"] }
+ed25519-dalek = { version = "2.1" }
hex = "0.4.3"
-rand = "0.8.5"
-x25519-dalek = "2.0.1"
+rand = "0.9.1"
+x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
cmds/generator-helper/src/main.rsdiffbeforeafterboth--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -11,10 +11,11 @@
};
use anyhow::{anyhow, bail, ensure, Context, Result};
use clap::{Parser, ValueEnum};
+use ed25519_dalek::SecretKey;
use fleet_shared::SecretData;
use rand::{
- distributions::{Alphanumeric, DistString, Distribution, Uniform},
- thread_rng, RngCore,
+ distr::{Alphanumeric, Distribution, SampleString, Uniform},
+ rng, RngCore,
};
fn write_output_file(out: &str) -> Result<File> {
@@ -224,7 +225,7 @@
fn main() -> Result<()> {
let opts = Opts::parse();
// Assumed to be secure, seeded from secure OsRng+reseeded.
- let mut rng = thread_rng();
+ let mut rng = rng();
match opts {
Opts::Public { output, encoding } => {
@@ -245,7 +246,10 @@
use ed25519_dalek::SigningKey;
let recipients = load_identities()?;
- let key = SigningKey::generate(&mut rng).to_keypair_bytes();
+ let mut secret = SecretKey::default();
+ rng.fill_bytes(&mut secret);
+ // TODO: Use SigningKey::generate after https://github.com/dalek-cryptography/curve25519-dalek/pull/762
+ let key = SigningKey::from_bytes(&secret).to_keypair_bytes();
write_public(&public, &key[32..], encoding)?;
write_private(
&recipients,
@@ -268,7 +272,8 @@
use x25519_dalek::{PublicKey, StaticSecret};
let recipients = load_identities()?;
- let key = StaticSecret::random_from_rng(rng);
+ // TODO: Use random_from_rng after https://github.com/dalek-cryptography/curve25519-dalek/pull/762
+ let key = StaticSecret::random();
let public_key: PublicKey = (&key).into();
write_public(&public, public_key.as_bytes().as_slice(), encoding)?;
write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;
@@ -289,7 +294,8 @@
} else {
// Alphabet of Alphanumberic + symbols
const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
- let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());
+ let uniform =
+ Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");
(0..size)
.map(|_| uniform.sample(&mut rng))
.map(|i| GEN_ASCII_SYMBOLS[i] as char)
@@ -310,7 +316,9 @@
let recipients = load_identities()?;
let mut bytes = vec![0u8; count];
if no_nuls {
- let rand = Uniform::new_inclusive(0x1u8, 0xffu8).sample_iter(&mut rng);
+ let rand = Uniform::new_inclusive(0x1u8, 0xffu8)
+ .expect("range is valid")
+ .sample_iter(&mut rng);
for (byte, rand) in bytes.iter_mut().zip(rand) {
*byte = rand;
}
cmds/terraform-provider-fleet/Cargo.tomldiffbeforeafterboth--- a/cmds/terraform-provider-fleet/Cargo.toml
+++ b/cmds/terraform-provider-fleet/Cargo.toml
@@ -9,5 +9,5 @@
serde = { workspace = true, features = ["derive"] }
tokio.workspace = true
-async-trait = "0.1.81"
+async-trait = "0.1.88"
tf-provider = "0.2.2"
crates/better-command/Cargo.tomldiffbeforeafterboth--- a/crates/better-command/Cargo.toml
+++ b/crates/better-command/Cargo.toml
@@ -5,7 +5,7 @@
rust-version.workspace = true
[dependencies]
-regex = "1.10"
+regex = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
crates/fleet-base/Cargo.tomldiffbeforeafterboth--- a/crates/fleet-base/Cargo.toml
+++ b/crates/fleet-base/Cargo.toml
@@ -8,21 +8,23 @@
age.workspace = true
anyhow.workspace = true
better-command.workspace = true
-chrono = "0.4.38"
+chrono = "0.4.41"
clap = { workspace = true, features = ["derive"] }
fleet-shared.workspace = true
-futures = "0.3.30"
-hostname = "0.4.0"
+futures = "0.3.31"
+hostname = "0.4.1"
indoc = "2.0.6"
-itertools = "0.13.0"
+itertools = "0.14.0"
nix-eval.workspace = true
nixlike.workspace = true
-nom = "7.1.3"
-openssh = "0.11.0"
-rand = "0.8.5"
+nom = "8.0.0"
+openssh = "0.11.5"
+rand = "0.9.1"
serde.workspace = true
-serde_json = "1.0.127"
+serde_json = "1.0.140"
+tabled = "0.20.0"
tempfile.workspace = true
+time = { version = "0.3.41", features = ["parsing"] }
tokio.workspace = true
-tokio-util = "0.7.11"
+tokio-util = "0.7.15"
tracing.workspace = true
crates/fleet-base/src/deploy.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/fleet-base/src/deploy.rs
@@ -0,0 +1,297 @@
+use std::{path::PathBuf, time::Duration};
+
+use anyhow::{anyhow, bail, Context as _, Result};
+use clap::ValueEnum;
+use itertools::Itertools;
+use tokio::time::sleep;
+use tracing::{error, info, info_span, warn, Instrument as _};
+
+use crate::host::{Config, ConfigHost, DeployKind, Generation, GenerationStorage};
+
+#[derive(ValueEnum, Clone, Copy)]
+pub enum DeployAction {
+ /// Upload derivation, but do not execute the update.
+ Upload,
+ /// Upload and execute the activation script, old version will be used after reboot.
+ Test,
+ /// Upload and set as current system profile, but do not execute activation script.
+ Boot,
+ /// Upload, set current profile, and execute activation script.
+ Switch,
+}
+
+impl DeployAction {
+ pub(crate) fn name(&self) -> Option<&'static str> {
+ match self {
+ Self::Upload => None,
+ Self::Test => Some("test"),
+ Self::Boot => Some("boot"),
+ Self::Switch => Some("switch"),
+ }
+ }
+ pub(crate) fn should_switch_profile(&self) -> bool {
+ matches!(self, Self::Switch | Self::Boot)
+ }
+ pub(crate) fn should_activate(&self) -> bool {
+ matches!(self, Self::Switch | Self::Test | Self::Boot)
+ }
+ pub(crate) fn should_create_rollback_marker(&self) -> bool {
+ // Upload does nothing on the target machine, other than uploading the closure.
+ // In boot case we want to have rollback marker prepared, so that the system may rollback itself on the next boot.
+ !matches!(self, Self::Upload)
+ }
+ pub(crate) fn should_schedule_rollback_run(&self) -> bool {
+ matches!(self, Self::Switch | Self::Test)
+ }
+}
+
+async fn get_current_generation(host: &ConfigHost) -> Result<Generation> {
+ let generations = host.list_generations("system").await?;
+ let current = generations
+ .into_iter()
+ .filter(|g| g.current)
+ .at_most_one()
+ .map_err(|_e| anyhow!("bad list-generations output"))?
+ .ok_or_else(|| anyhow!("failed to find generation"))?;
+ Ok(current)
+}
+
+pub async fn deploy_task(
+ action: DeployAction,
+ host: &ConfigHost,
+ built: PathBuf,
+ specialisation: Option<String>,
+ disable_rollback: bool,
+) -> Result<()> {
+ let deploy_kind = host.deploy_kind().await?;
+ if (deploy_kind == DeployKind::NixosInstall || deploy_kind == DeployKind::NixosLustrate)
+ && !matches!(action, DeployAction::Boot | DeployAction::Upload)
+ {
+ bail!("{deploy_kind:?} deploy kind only supports boot and upload actions");
+ }
+
+ let mut failed = false;
+
+ // TODO: Lockfile, to prevent concurrent system switch?
+ // TODO: If rollback target exists - bail, it should be removed. Lockfile will not work in case if rollback
+ // is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to
+ // unit name conflict in systemd-run
+ // This code is tied to rollback.nix
+ if !disable_rollback && action.should_create_rollback_marker() {
+ let _span = info_span!("preparing").entered();
+ info!("preparing for rollback");
+ let generation = get_current_generation(host).await?;
+ info!(
+ "rollback target would be {} {}",
+ generation.id, generation.datetime
+ );
+ {
+ let mut cmd = host.cmd("sh").await?;
+ cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));
+ if let Err(e) = cmd.sudo().run().await {
+ error!("failed to set rollback marker: {e}");
+ failed = true;
+ }
+ }
+ // Activation script also starts rollback-watchdog.timer, however, it is possible that it won't be started.
+ // Kicking it on manually will work best.
+ //
+ // There wouldn't be conflict, because here we trigger start of the primary service, and systemd will
+ // only allow one instance of it.
+
+ // TODO: We should also watch how this process is going.
+ // After running this command, we have less than 3 minutes to deploy everything,
+ // if we fail to perform generation switch in time, then we will still call the activation script, and this may break something.
+ // Anyway, reboot will still help in this case.
+ if action.should_schedule_rollback_run() {
+ let mut cmd = host.cmd("systemd-run").await?;
+ cmd.comparg("--on-active", "3min")
+ .comparg("--unit", "rollback-watchdog-run")
+ .arg("systemctl")
+ .arg("start")
+ .arg("rollback-watchdog.service");
+ if let Err(e) = cmd.sudo().run().await {
+ error!("failed to schedule rollback run: {e}");
+ failed = true;
+ }
+ }
+ }
+ if deploy_kind == DeployKind::NixosLustrate {
+ // Fleet could also create this file, but as this operation is potentially disruptive,
+ // make user do it themself.
+ if !host.file_exists("/etc/NIXOS_LUSTRATE").await? {
+ bail!("/etc/NIXOS_LUSTRATE should be created on remote host");
+ }
+ // Wanted by NixOS to recognize the system as NixOS.
+ let mut cmd = host.cmd("touch").await?;
+ cmd.arg("/etc/NIXOS");
+ cmd.sudo().run().await.context("creating /etc/NIXOS")?;
+ }
+ if deploy_kind == DeployKind::NixosInstall {
+ info!(
+ "running nixos-install to switch profile, install bootloader, and perform activation"
+ );
+ let mut cmd = host.cmd("nixos-install").await?;
+ cmd.arg("--system").arg(&built).args([
+ // Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
+ // It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
+ "--no-channel-copy",
+ "--root",
+ "/mnt",
+ ]);
+ if let Err(e) = cmd.sudo().run().await {
+ error!("failed to execute nixos-install: {e}");
+ failed = true;
+ }
+ } else {
+ if action.should_switch_profile() && !failed {
+ info!("switching system profile generation");
+
+ // To avoid even more problems, using nixos-install for now.
+ // // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
+ // // falling back to using nix-env command
+ // // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
+ // let mut cmd = host.cmd("nix-env").await?;
+ // cmd.args([
+ // "--store",
+ // "/mnt",
+ // "--profile",
+ // "/mnt/nix/var/nix/profiles/system",
+ // "--set",
+ // ])
+ // .arg(&built);
+ // if let Err(e) = cmd.sudo().run_nix().await {
+ // error!("failed to switch system profile generation: {e}");
+ // failed = true;
+ // }
+ // It would also be possible to update profile atomically during copy:
+ // https://github.com/NixOS/nix/pull/11657
+ let mut cmd = host.nix_cmd().await?;
+ cmd.arg("build");
+ cmd.comparg("--profile", "/nix/var/nix/profiles/system");
+ cmd.arg(&built);
+ if let Err(e) = cmd.sudo().run_nix().await {
+ error!("failed to switch system profile generation: {e}");
+ failed = true;
+ }
+ }
+
+ // FIXME: Connection might be disconnected after activation run
+
+ if action.should_activate() && !failed {
+ let _span = info_span!("activating").entered();
+ info!("executing activation script");
+ let specialised = if let Some(specialisation) = specialisation {
+ let mut specialised = built.join("specialisation");
+ specialised.push(specialisation);
+ specialised
+ } else {
+ built.clone()
+ };
+ let switch_script = specialised.join("bin/switch-to-configuration");
+ let mut cmd = host.cmd(switch_script).in_current_span().await?;
+ if deploy_kind == DeployKind::NixosLustrate {
+ cmd.env("NIXOS_INSTALL_BOOTLOADER", "1");
+ }
+ cmd.env("FLEET_ONLINE_ACTIVATION", "1")
+ .arg(action.name().expect("upload.should_activate == false"));
+ if let Err(e) = cmd.sudo().run().in_current_span().await {
+ error!("failed to activate: {e}");
+ failed = true;
+ }
+ }
+ }
+ if action.should_create_rollback_marker() {
+ if !disable_rollback {
+ if failed {
+ if action.should_schedule_rollback_run() {
+ info!("executing rollback");
+ if let Err(e) = host
+ .systemctl_start("rollback-watchdog.service")
+ .instrument(info_span!("rollback"))
+ .await
+ {
+ error!("failed to trigger rollback: {e}")
+ }
+ }
+ } else {
+ info!("trying to mark upgrade as successful");
+ if let Err(e) = host
+ .rm_file("/etc/fleet_rollback_marker", true)
+ .in_current_span()
+ .await
+ {
+ 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");
+ if let Err(_e) = host.systemctl_stop("rollback-watchdog.timer").await {
+ // It is ok, if there was no reboot - then timer might not be running.
+ }
+ if action.should_schedule_rollback_run() {
+ if let Err(e) = host.systemctl_stop("rollback-watchdog-run.timer").await {
+ 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.
+ }
+ }
+ Ok(())
+}
+
+pub async fn upload_task(
+ config: &Config,
+ host: &ConfigHost,
+ location: GenerationStorage,
+ generation: PathBuf,
+) -> Result<PathBuf> {
+ let local_host = config.local_host();
+ if matches!(location, GenerationStorage::Pusher) {
+ bail!("pusher is not enabled in this version of fleet");
+ }
+ if !host.local {
+ info!("uploading system closure");
+ {
+ // TODO: Move to remote_derivation method.
+ // Alternatively, nix store make-content-addressed can be used,
+ // at least for the first deployment, to provide trusted store key.
+ //
+ // It is much slower, yet doesn't require root on the deployer machine.
+ let Ok(mut sign) = local_host.cmd("nix").await else {
+ bail!("failed to setup local");
+ };
+ // Private key for host machine is registered in nix-sign.nix
+ sign.arg("store")
+ .arg("sign")
+ .comparg("--key-file", "/etc/nix/private-key")
+ .arg("-r")
+ .arg(&generation);
+ if let Err(e) = sign.sudo().run_nix().await {
+ warn!("failed to sign store paths: {e}");
+ };
+ }
+ let mut tries = 0;
+ loop {
+ match host.remote_derivation(&generation).await {
+ Ok(remote) => {
+ assert!(remote == generation, "CA derivations aren't implemented");
+ return Ok(remote);
+ }
+ Err(e) if tries < 3 => {
+ tries += 1;
+ warn!("copy failure ({}/3): {}", tries, e);
+ sleep(Duration::from_millis(5000)).await;
+ }
+ Err(e) => {
+ bail!("upload failed: {e}");
+ }
+ }
+ }
+ }
+ Ok(generation)
+}
crates/fleet-base/src/fleetdata.rsdiffbeforeafterboth--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -7,8 +7,8 @@
use chrono::{DateTime, Utc};
use fleet_shared::SecretData;
use rand::{
- distributions::{Alphanumeric, DistString},
- thread_rng,
+ distr::{Alphanumeric, SampleString as _},
+ rng,
};
use serde::{de::Error, Deserialize, Serialize};
use serde_json::Value;
@@ -47,7 +47,7 @@
}
fn generate_gc_prefix() -> String {
- let id = Alphanumeric.sample_string(&mut thread_rng(), 8);
+ let id = Alphanumeric.sample_string(&mut rng(), 8);
format!("fleet-gc-{id}")
}
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 tempfile::NamedTempFile;1920use crate::{21 command::MyCommand,22 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},23};2425pub struct FleetConfigInternals {26 /// Fleet project directory, containing fleet.nix file.27 pub directory: PathBuf,28 /// builtins.currentSystem29 pub local_system: String,30 pub data: Mutex<FleetData>,31 pub nix_args: Vec<OsString>,32 /// fleet_config.config33 pub config_field: Value,34 // TODO: Remove with connectivity refactor35 pub localhost: String,3637 /// import nixpkgs {system = local};38 pub default_pkgs: Value,39 /// inputs.nixpkgs40 pub nixpkgs: Value,4142 pub nix_session: NixSession,43}4445// TODO: Make field not pub46#[derive(Clone)]47pub struct Config(pub Arc<FleetConfigInternals>);4849impl Deref for Config {50 type Target = FleetConfigInternals;5152 fn deref(&self) -> &Self::Target {53 &self.054 }55}5657#[derive(Clone, Copy, Debug)]58pub enum EscalationStrategy {59 Sudo,60 Run0,61 Su,62}6364#[derive(Clone, PartialEq, Copy, Debug)]65pub enum DeployKind {66 /// NixOS => NixOS managed by fleet67 UpgradeToFleet,68 /// NixOS managed by fleet => NixOS managed by fleet69 Fleet,70 /// Remote host has /mnt, /mnt/boot mounted,71 /// generated config is added to fleet configuration.72 NixosInstall,73 /// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),74 /// generated config is added to fleet configuration,75 /// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.76 NixosLustrate,77}7879impl FromStr for DeployKind {80 type Err = anyhow::Error;81 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {82 match s {83 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),84 "fleet" => Ok(Self::Fleet),85 "nixos-install" => Ok(Self::NixosInstall),86 "nixos-lustrate" => Ok(Self::NixosLustrate),87 v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""),88 }89 }90}91pub struct ConfigHost {92 config: Config,93 pub name: String,94 groups: OnceCell<Vec<String>>,9596 deploy_kind: OnceCell<DeployKind>,9798 pub host_config: Option<Value>,99 pub nixos_config: OnceCell<Value>,100 pub nixos_unchecked_config: OnceCell<Value>,101 pub pkgs_override: Option<Value>,102103 // TODO: Move command helpers away with connectivity refactor104 pub local: bool,105 pub session: OnceLock<Arc<openssh::Session>>,106}107// TODO: Move command helpers away with connectivity refactor108impl ConfigHost {109 pub fn set_deploy_kind(&self, kind: DeployKind) {110 self.deploy_kind111 .set(kind)112 .ok()113 .expect("deploy kind is already set");114 }115 pub async fn deploy_kind(&self) -> Result<DeployKind> {116 if let Some(kind) = self.deploy_kind.get() {117 return Ok(kind.clone());118 }119 let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {120 Ok(v) => v,121 Err(e) => {122 bail!("failed to query remote system kind: {}", e);123 }124 };125 if !is_fleet_managed {126 bail!(indoc::indoc! {"127 host is not marked as managed by fleet128 if you're not trying to lustrate/install system from scratch,129 you should either130 1. manually create /etc/FLEET_HOST file on the target host,131 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet132 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos133 "});134 }135 // TOCTOU is possible136 let _ = self.deploy_kind.set(DeployKind::Fleet);137 Ok(self138 .deploy_kind139 .get()140 .expect("deploy kind is just set")141 .clone())142 }143 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {144 // Prefer sudo, as run0 has some gotchas with polkit145 // and too many repeating prompts.146 if (self.find_in_path("sudo").await).is_ok() {147 return Ok(EscalationStrategy::Sudo);148 }149 if (self.find_in_path("run0").await).is_ok() {150 return Ok(EscalationStrategy::Run0);151 }152 Ok(EscalationStrategy::Su)153 }154 async fn open_session(&self) -> Result<Arc<openssh::Session>> {155 assert!(!self.local, "do not open ssh connection to local session");156 // FIXME: TOCTOU157 if let Some(session) = &self.session.get() {158 return Ok((*session).clone());159 };160 let session = SessionBuilder::default();161 let session = session162 .connect(&self.name)163 .await164 .map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;165 let session = Arc::new(session);166 self.session.set(session.clone()).expect("TOCTOU happened");167 Ok(session)168 }169 pub async fn mktemp_dir(&self) -> Result<String> {170 let mut cmd = self.cmd("mktemp").await?;171 cmd.arg("-d");172 let path = cmd.run_string().await?;173 Ok(path.trim_end().to_owned())174 }175 pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {176 let mut cmd = self.cmd("sh").await?;177 cmd.arg("-c")178 .arg("test -e \"$1\" && echo true || echo false")179 .arg("_")180 .arg(path);181 Ok(cmd.run_value().await?)182 }183 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {184 let mut cmd = self.cmd("cat").await?;185 cmd.arg(path);186 cmd.run_bytes().await187 }188 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {189 let mut cmd = self.cmd("cat").await?;190 cmd.arg(path);191 cmd.run_string().await192 }193 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {194 let mut cmd = self.cmd("ls").await?;195 cmd.arg(path);196 let out = cmd.run_string().await?;197 let mut lines = out.split('\n');198 if let Some(last) = lines.next_back() {199 ensure!(last.is_empty(), "output of ls should end with newline");200 }201 Ok(lines.map(ToOwned::to_owned).collect())202 }203 #[allow(dead_code)]204 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {205 let text = self.read_file_text(path).await?;206 Ok(serde_json::from_str(&text)?)207 }208 pub async fn read_env(&self, env: &str) -> Result<String> {209 let mut cmd = self.cmd("printenv").await?;210 cmd.arg(env);211 cmd.run_string().await212 }213 pub async fn find_in_path(&self, command: &str) -> Result<String> {214 // // `which` is not a part of coreutils, and it might not exist on machine.215 // let path = self.read_env("PATH").await?;216 // // Assuming delimiter is :, we don't work with windows host, this check will be much217 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)218 // for ele in path.split(':') {219 // let test_path = format!("{ele}/{cmd}");220 // test -x etc221 // }222 // let mut cmd = self.cmd("printenv").await?;223 // cmd.arg(env);224 // Ok(cmd.run_string().await?)225 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.226 let mut cmd = self227 .cmd_escalation(228 // Not used229 EscalationStrategy::Su,230 "which",231 )232 .await?;233 cmd.arg(command);234 cmd.run_string().await235 }236 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>237 where238 <D as FromStr>::Err: Display,239 {240 let text = self.read_file_text(path).await?;241 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))242 }243 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {244 self.cmd_escalation(self.escalation_strategy().await?, cmd)245 .await246 }247 pub async fn cmd_escalation(248 &self,249 escalation: EscalationStrategy,250 cmd: impl AsRef<OsStr>,251 ) -> Result<MyCommand> {252 if self.local {253 Ok(MyCommand::new(escalation, cmd))254 } else {255 let session = self.open_session().await?;256 Ok(MyCommand::new_on(escalation, cmd, session))257 }258 }259 pub async fn nix_cmd(&self) -> Result<MyCommand> {260 let mut nix = self.cmd("nix").await?;261 nix.args([262 "--extra-experimental-features",263 "nix-command",264 "--extra-experimental-features",265 "flakes",266 ]);267 Ok(nix)268 }269270 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {271 ensure!(data.encrypted, "secret is not encrypted");272 let mut cmd = self.cmd("fleet-install-secrets").await?;273 cmd.arg("decrypt").eqarg("--secret", data.to_string());274 let encoded = cmd275 .sudo()276 .run_string()277 .await278 .context("failed to call remote host for decrypt")?;279 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;280 ensure!(!data.encrypted, "secret came out encrypted");281 Ok(data.data)282 }283 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {284 ensure!(data.encrypted, "secret is not encrypted");285 let mut cmd = self.cmd("fleet-install-secrets").await?;286 cmd.arg("reencrypt").eqarg("--secret", data.to_string());287 for target in targets {288 let key = self.config.key(&target).await?;289 cmd.eqarg("--targets", key);290 }291 let encoded = cmd292 .sudo()293 .run_string()294 .await295 .context("failed to call remote host for decrypt")?;296 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;297 ensure!(data.encrypted, "secret came out not encrypted");298 Ok(data)299 }300 /// Returns path for futureproofing, as path might change i.e on conversion to CA301 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {302 if self.local {303 // Path is located locally, thus already trusted.304 return Ok(path.to_owned());305 }306 let mut nix = MyCommand::new(307 // Not used308 EscalationStrategy::Su,309 "nix",310 );311 nix.arg("copy").arg("--substitute-on-destination");312313 match self.deploy_kind().await? {314 DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {315 nix.comparg("--to", format!("ssh-ng://{}", self.name));316 }317 DeployKind::NixosInstall => {318 nix319 // Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon320 .arg("--no-check-sigs")321 .comparg(322 "--to",323 format!("ssh-ng://root@{}?remote-store=/mnt", self.name),324 );325 }326 }327 nix.arg(path);328 nix.run_nix().await.context("nix copy")?;329 Ok(path.to_owned())330 }331 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {332 let mut cmd = self.cmd("systemctl").await?;333 cmd.arg("stop").arg(name);334 cmd.sudo().run().await335 }336 pub async fn systemctl_start(&self, name: &str) -> Result<()> {337 let mut cmd = self.cmd("systemctl").await?;338 cmd.arg("start").arg(name);339 cmd.sudo().run().await340 }341342 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {343 let mut cmd = self.cmd("rm").await?;344 cmd.arg("-f").arg(path);345 if sudo {346 cmd = cmd.sudo()347 }348 cmd.run().await349 }350}351impl ConfigHost {352 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,353 // assuming getting tags always returns the same value.354 pub async fn tags(&self) -> Result<Vec<String>> {355 if let Some(v) = self.groups.get() {356 return Ok(v.clone());357 }358 let Some(host_config) = &self.host_config else {359 return Ok(vec![]);360 };361 let tags: Vec<String> = nix_go_json!(host_config.tags);362363 let _ = self.groups.set(tags.clone());364365 Ok(tags)366 }367 pub async fn nixos_config(&self) -> Result<Value> {368 if let Some(v) = self.nixos_config.get() {369 return Ok(v.clone());370 }371 let Some(host_config) = &self.host_config else {372 bail!("local host has no nixos_config");373 };374 let nixos_config = nix_go!(host_config.nixos.config);375 assert_warn("nixos config evaluation", &nixos_config).await?;376377 let _ = self.nixos_config.set(nixos_config.clone());378379 Ok(nixos_config)380 }381 pub async fn nixos_unchecked_config(&self) -> Result<Value> {382 if let Some(v) = self.nixos_unchecked_config.get() {383 return Ok(v.clone());384 }385 let Some(host_config) = &self.host_config else {386 bail!("local host has no nixos_config");387 };388 let nixos_config = nix_go!(host_config.nixos_unchecked.config);389390 let _ = self.nixos_unchecked_config.set(nixos_config.clone());391392 Ok(nixos_config)393 }394395 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {396 let nixos = self.nixos_unchecked_config().await?;397 let secrets = nix_go!(nixos.secrets);398 let mut out = Vec::new();399 for name in secrets.list_fields().await? {400 let secret = nix_go!(secrets[{ name }]);401 let is_shared: bool = nix_go_json!(secret.shared);402 if is_shared {403 continue;404 }405 out.push(name);406 }407 Ok(out)408 }409 pub async fn secret_field(&self, name: &str) -> Result<Value> {410 let nixos = self.nixos_unchecked_config().await?;411 Ok(nix_go!(nixos.secrets[{ name }]))412 }413414 /// Packages for this host, resolved with nixpkgs overlays415 pub async fn pkgs(&self) -> Result<Value> {416 if let Some(value) = &self.pkgs_override {417 return Ok(value.clone());418 }419 let Some(host_config) = &self.host_config else {420 bail!("local host has no host_config");421 };422 // TODO: Should nixos.options be cached?423 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))424 }425}426427impl Config {428 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {429 let config = &self.config_field;430 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);431 Ok(tagged)432 }433 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {434 let mut out = BTreeSet::new();435 for owner in owners {436 if let Some(tag) = owner.strip_prefix('@') {437 let hosts = self.tagged_hostnames(tag).await?;438 out.extend(hosts);439 } else {440 out.insert(owner);441 }442 }443 Ok(out)444 }445 pub fn local_host(&self) -> ConfigHost {446 ConfigHost {447 config: self.clone(),448 name: "<virtual localhost>".to_owned(),449 host_config: None,450 nixos_config: OnceCell::new(),451 nixos_unchecked_config: OnceCell::new(),452 groups: {453 let cell = OnceCell::new();454 let _ = cell.set(vec![]);455 cell456 },457 pkgs_override: Some(self.default_pkgs.clone()),458459 local: true,460 session: OnceLock::new(),461 deploy_kind: OnceCell::new(),462 }463 }464465 pub async fn host(&self, name: &str) -> Result<ConfigHost> {466 let config = &self.config_field;467 let host_config = nix_go!(config.hosts[{ name }]);468469 Ok(ConfigHost {470 config: self.clone(),471 name: name.to_owned(),472 host_config: Some(host_config),473 nixos_config: OnceCell::new(),474 nixos_unchecked_config: OnceCell::new(),475 groups: OnceCell::new(),476 pkgs_override: None,477478 // TODO: Remove with connectivit refactor479 local: self.localhost == name,480 session: OnceLock::new(),481 deploy_kind: OnceCell::new(),482 })483 }484 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {485 let config = &self.config_field;486 let names = nix_go!(config.hosts).list_fields().await?;487 let mut out = vec![];488 for name in names {489 out.push(self.host(&name).await?);490 }491 Ok(out)492 }493 // TODO: Replace usages with .host().nixos_config494 pub async fn system_config(&self, host: &str) -> Result<Value> {495 let fleet_field = &self.config_field;496 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))497 }498499 /// Shared secrets configured in fleet.nix or in flake500 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {501 let config_field = &self.config_field;502 Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)503 }504 /// Shared secrets configured in fleet.nix505 pub fn list_shared(&self) -> Vec<String> {506 let data = self.data();507 data.shared_secrets.keys().cloned().collect()508 }509 pub fn has_shared(&self, name: &str) -> bool {510 let data = self.data();511 data.shared_secrets.contains_key(name)512 }513 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {514 let mut data = self.data_mut();515 data.shared_secrets.insert(name.to_owned(), shared);516 }517 pub fn remove_shared(&self, secret: &str) {518 let mut data = self.data_mut();519 data.shared_secrets.remove(secret);520 }521522 pub fn list_secrets(&self, host: &str) -> Vec<String> {523 let data = self.data();524 let Some(secrets) = data.host_secrets.get(host) else {525 return Vec::new();526 };527 secrets.keys().cloned().collect()528 }529530 pub fn has_secret(&self, host: &str, secret: &str) -> bool {531 let data = self.data();532 let Some(host_secrets) = data.host_secrets.get(host) else {533 return false;534 };535 host_secrets.contains_key(secret)536 }537 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {538 let mut data = self.data_mut();539 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();540 host_secrets.insert(secret, value);541 }542543 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {544 let data = self.data();545 let Some(host_secrets) = data.host_secrets.get(host) else {546 bail!("no secrets for machine {host}");547 };548 let Some(secret) = host_secrets.get(secret) else {549 bail!("machine {host} has no secret {secret}");550 };551 Ok(secret.clone())552 }553 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {554 let data = self.data();555 let Some(secret) = data.shared_secrets.get(secret) else {556 bail!("no shared secret {secret}");557 };558 Ok(secret.clone())559 }560 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {561 let config_field = &self.config_field;562 Ok(nix_go_json!(563 config_field.sharedSecrets[{ secret }].expectedOwners564 ))565 }566567 // TODO: Should this be something modifiable from other processes?568 // E.g terraform provider might want to update FleetData (e.g secrets),569 // and current implementation assumes only one process holds current fleet.nix570 // Given that it is no longer needs to be a file for nix evaluation,571 // maybe it can be a .nix file for persistence, but accessible only572 // thru some shared state controller? Might it be stored in terraform573 // state provider?574 pub fn data(&self) -> MutexGuard<FleetData> {575 self.data.lock().unwrap()576 }577 pub fn data_mut(&self) -> MutexGuard<FleetData> {578 self.data.lock().unwrap()579 }580 pub fn save(&self) -> Result<()> {581 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.")?;582 let data = nixlike::serialize(&self.data() as &FleetData)?;583 tempfile.write_all(584 format!(585 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",586 data587 )588 .as_bytes(),589 )?;590 let mut fleet_data_path = self.directory.clone();591 fleet_data_path.push("fleet.nix");592 tempfile.persist(fleet_data_path)?;593 Ok(())594 }595}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::{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}crates/fleet-base/src/lib.rsdiffbeforeafterboth--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -3,3 +3,4 @@
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
@@ -7,7 +7,6 @@
};
use anyhow::{bail, Context, Result};
-use clap::Parser;
use nix_eval::{nix_go, util::assert_warn, NixSessionPool, Value};
use nom::{
bytes::complete::take_while1,
@@ -15,6 +14,7 @@
combinator::{map, opt},
multi::separated_list1,
sequence::{preceded, separated_pair},
+ Parser,
};
use crate::{
@@ -38,11 +38,13 @@
err.to_string()
}
- let (input, is_tag) = map(opt(char('@')), |c| c.is_some())(input).map_err(err_to_string)?;
+ let (input, is_tag) = map(opt(char('@')), |c| c.is_some())
+ .parse_complete(input)
+ .map_err(err_to_string)?;
let (input, name) = map(
take_while1(|v| v != ',' && v != '?' && v != '@'),
str::to_owned,
- )(input)
+ ).parse_complete(input)
.map_err(err_to_string)?;
let kw_item = separated_pair(
@@ -55,7 +57,7 @@
});
let mut opt_kw = map(opt(preceded(char('?'), kw)), Option::unwrap_or_default);
- let (input, attrs) = opt_kw(input).map_err(err_to_string)?;
+ let (input, attrs) = opt_kw.parse_complete(input).map_err(err_to_string)?;
if !input.is_empty() {
return Err(format!("unexpected trailing input: {input:?}"));
@@ -68,7 +70,7 @@
}
// TODO: Rename to HostSelector
-#[derive(Parser, Clone)]
+#[derive(clap::Parser, Clone)]
pub struct FleetOpts {
/// All hosts except those would be skipped
#[clap(long, number_of_values = 1, value_parser = host_item_parser)]
crates/fleet-shared/Cargo.tomldiffbeforeafterboth--- a/crates/fleet-shared/Cargo.toml
+++ b/crates/fleet-shared/Cargo.toml
@@ -6,6 +6,6 @@
[dependencies]
base64 = "0.22.1"
-serde = "1.0.202"
+serde = "1.0.219"
unicode_categories = "0.1.1"
-z85 = "3.0.5"
+z85 = "3.0.6"
crates/nix-eval/Cargo.tomldiffbeforeafterboth--- a/crates/nix-eval/Cargo.toml
+++ b/crates/nix-eval/Cargo.toml
@@ -16,11 +16,11 @@
tokio-util.workspace = true
tracing.workspace = true
-futures = "0.3.30"
-itertools = "0.13.0"
+futures = "0.3.31"
+itertools = "0.14.0"
r2d2 = "0.8.10"
-regex = "1.10.6"
-unindent = "0.2.3"
+regex = "1.11.1"
+unindent = "0.2.4"
# [build-dependencies]
# bindgen = "0.69.4"
crates/nixlike/Cargo.tomldiffbeforeafterboth--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -9,8 +9,8 @@
alejandra = { git = "https://github.com/kamadorueda/alejandra" }
linked-hash-map = "0.5.6"
-peg = "0.8.2"
-ron = "0.8.1"
-serde = "1.0.196"
+peg = "0.8.5"
+ron = "0.10.1"
+serde = "1.0.219"
serde-transcode = "1.1.1"
-serde_json = "1.0.113"
+serde_json = "1.0.140"