ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

switch to miette + rehaul errors (#209)

authored by

marshmallow and committed by
GitHub
aa314028 8c17aaef

+773 -175
+2
CHANGELOG.md
··· 12 12 - Added `--reboot`. Wire will wait for the node to reconnect after rebooting. 13 13 Wire will refuse to reboot localhost. Keys post-activation will be applied 14 14 after rebooting! 15 + - Most errors now have error codes and documentation links. 15 16 16 17 ### Changed 17 18 ··· 20 21 dry-activate). 21 22 - Nix logs with the `Talkative` and `Chatty` level have been moved to 22 23 `tracing_level::TRACE`. 24 + - Error messages have been greatly improved. 23 25 24 26 ## [0.4.0] - 2025-07-10 25 27
+104 -1
Cargo.lock
··· 121 121 ] 122 122 123 123 [[package]] 124 + name = "backtrace-ext" 125 + version = "0.2.1" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" 128 + dependencies = [ 129 + "backtrace", 130 + ] 131 + 132 + [[package]] 124 133 name = "beef" 125 134 version = "0.5.2" 126 135 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 541 550 ] 542 551 543 552 [[package]] 553 + name = "is_ci" 554 + version = "1.2.0" 555 + source = "registry+https://github.com/rust-lang/crates.io-index" 556 + checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" 557 + 558 + [[package]] 544 559 name = "is_terminal_polyfill" 545 560 version = "1.70.1" 546 561 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 597 612 "futures", 598 613 "gethostname", 599 614 "im", 615 + "itertools", 600 616 "key_agent", 617 + "miette", 601 618 "nix", 619 + "proc-macro2", 602 620 "prost", 603 621 "regex", 604 622 "serde", 605 623 "serde-query", 606 624 "serde_json", 607 625 "serde_repr", 626 + "syn 2.0.105", 608 627 "tempdir", 609 628 "thiserror", 610 629 "tokio", ··· 671 690 checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 672 691 673 692 [[package]] 693 + name = "miette" 694 + version = "7.6.0" 695 + source = "registry+https://github.com/rust-lang/crates.io-index" 696 + checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 697 + dependencies = [ 698 + "backtrace", 699 + "backtrace-ext", 700 + "cfg-if", 701 + "miette-derive", 702 + "owo-colors", 703 + "supports-color", 704 + "supports-hyperlinks", 705 + "supports-unicode", 706 + "terminal_size", 707 + "textwrap", 708 + "unicode-width 0.1.14", 709 + ] 710 + 711 + [[package]] 712 + name = "miette-derive" 713 + version = "7.6.0" 714 + source = "registry+https://github.com/rust-lang/crates.io-index" 715 + checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" 716 + dependencies = [ 717 + "proc-macro2", 718 + "quote", 719 + "syn 2.0.105", 720 + ] 721 + 722 + [[package]] 674 723 name = "miniz_oxide" 675 724 version = "0.8.9" 676 725 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 761 810 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 762 811 763 812 [[package]] 813 + name = "owo-colors" 814 + version = "4.2.2" 815 + source = "registry+https://github.com/rust-lang/crates.io-index" 816 + checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" 817 + 818 + [[package]] 764 819 name = "parking_lot" 765 820 version = "0.12.4" 766 821 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1198 1253 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1199 1254 1200 1255 [[package]] 1256 + name = "supports-color" 1257 + version = "3.0.2" 1258 + source = "registry+https://github.com/rust-lang/crates.io-index" 1259 + checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" 1260 + dependencies = [ 1261 + "is_ci", 1262 + ] 1263 + 1264 + [[package]] 1265 + name = "supports-hyperlinks" 1266 + version = "3.1.0" 1267 + source = "registry+https://github.com/rust-lang/crates.io-index" 1268 + checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" 1269 + 1270 + [[package]] 1271 + name = "supports-unicode" 1272 + version = "3.0.0" 1273 + source = "registry+https://github.com/rust-lang/crates.io-index" 1274 + checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" 1275 + 1276 + [[package]] 1201 1277 name = "syn" 1202 1278 version = "1.0.109" 1203 1279 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1243 1319 ] 1244 1320 1245 1321 [[package]] 1322 + name = "terminal_size" 1323 + version = "0.4.2" 1324 + source = "registry+https://github.com/rust-lang/crates.io-index" 1325 + checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" 1326 + dependencies = [ 1327 + "rustix", 1328 + "windows-sys 0.59.0", 1329 + ] 1330 + 1331 + [[package]] 1332 + name = "textwrap" 1333 + version = "0.16.2" 1334 + source = "registry+https://github.com/rust-lang/crates.io-index" 1335 + checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 1336 + dependencies = [ 1337 + "unicode-linebreak", 1338 + "unicode-width 0.2.1", 1339 + ] 1340 + 1341 + [[package]] 1246 1342 name = "thiserror" 1247 1343 version = "2.0.14" 1248 1344 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1388 1484 version = "1.0.18" 1389 1485 source = "registry+https://github.com/rust-lang/crates.io-index" 1390 1486 checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1487 + 1488 + [[package]] 1489 + name = "unicode-linebreak" 1490 + version = "0.1.5" 1491 + source = "registry+https://github.com/rust-lang/crates.io-index" 1492 + checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 1391 1493 1392 1494 [[package]] 1393 1495 name = "unicode-width" ··· 1725 1827 name = "wire" 1726 1828 version = "0.5.0" 1727 1829 dependencies = [ 1728 - "anyhow", 1729 1830 "clap", 1730 1831 "clap-markdown", 1731 1832 "clap-num", ··· 1738 1839 "indicatif", 1739 1840 "itertools", 1740 1841 "lib", 1842 + "miette", 1741 1843 "serde", 1742 1844 "serde_json", 1845 + "thiserror", 1743 1846 "tokio", 1744 1847 "tracing", 1745 1848 "tracing-indicatif",
+2
Cargo.toml
··· 21 21 anyhow = "1.0.98" 22 22 prost = "0.14.1" 23 23 nix = { version = "0.30.1", features = ["user"] } 24 + miette = { version = "7.6.0", features = ["fancy"] } 25 + thiserror = "2.0.12"
+1
doc/.vitepress/config.ts
··· 67 67 { text: "CLI", link: "/reference/cli" }, 68 68 { text: "Meta Options", link: "/reference/meta" }, 69 69 { text: "Module Options", link: "/reference/module" }, 70 + { text: "Error Codes", link: "/reference/errors" }, 70 71 ], 71 72 }, 72 73 ],
+3 -1
doc/default.nix
··· 6 6 ... 7 7 }: 8 8 { 9 - packages.docs = pkgs.callPackage ./package.nix { inherit (self'.packages) wire-small; }; 9 + packages.docs = pkgs.callPackage ./package.nix { 10 + inherit (self'.packages) wire-small wire-dignostics-md; 11 + }; 10 12 }; 11 13 }
+2
doc/package.nix
··· 3 3 nixosOptionsDoc, 4 4 runCommand, 5 5 wire-small, 6 + wire-dignostics-md, 6 7 nix, 7 8 nodejs, 8 9 pnpm, ··· 54 55 }; 55 56 patchPhase = '' 56 57 cat ${optionsDoc} >> ./reference/module.md 58 + cat ${wire-dignostics-md} >> ./reference/errors.md 57 59 wire inspect --markdown-help > ./reference/cli.md 58 60 ''; 59 61 buildPhase = "pnpm run build > build.log 2>&1";
+9
doc/reference/errors.md
··· 1 + --- 2 + comment: true 3 + title: Error Codes 4 + description: Most error codes and their associated documentation. 5 + --- 6 + 7 + # {{ $frontmatter.title }} 8 + 9 + {{ $frontmatter.description }}
+2 -1
wire/cli/Cargo.toml
··· 17 17 tracing-subscriber = { workspace = true } 18 18 lib = { path = "../lib" } 19 19 serde_json = { workspace = true } 20 - anyhow = { workspace = true } 20 + miette = { workspace = true } 21 + thiserror = { workspace = true } 21 22 indicatif = "0.18.0" 22 23 enum-display-derive = "0.1.1" 23 24 im = { workspace = true }
+7
wire/cli/default.nix
··· 55 55 ''; 56 56 meta.mainProgram = "wire"; 57 57 }; 58 + 59 + wire-dignostics-md = self'.packages.wire-unwrapped.overrideAttrs { 60 + DIAGNOSTICS_MD_OUTPUT = "/build/source"; 61 + installPhase = '' 62 + mv /build/source/DIAGNOSTICS.md $out 63 + ''; 64 + }; 58 65 }; 59 66 }; 60 67 }
+24 -14
wire/cli/src/apply.rs
··· 1 - use anyhow::anyhow; 2 1 use futures::{FutureExt, StreamExt}; 3 2 use indicatif::ProgressStyle; 4 3 use itertools::{Either, Itertools}; 5 - use lib::SubCommandModifiers; 6 4 use lib::hive::Hive; 7 - use lib::hive::node::{Context, GoalExecutor, StepState}; 5 + use lib::hive::node::{Context, GoalExecutor, Name, StepState}; 6 + use lib::{SubCommandModifiers, errors::HiveLibError}; 7 + use miette::{Diagnostic, Result}; 8 8 use std::collections::HashSet; 9 - use std::fmt::Write; 10 9 use std::path::PathBuf; 10 + use thiserror::Error; 11 11 use tracing::{Span, error, info, instrument}; 12 12 use tracing_indicatif::span_ext::IndicatifSpanExt; 13 13 14 14 use crate::cli::{ApplyArgs, ApplyTarget}; 15 15 16 + #[derive(Debug, Error, Diagnostic)] 17 + #[error("node {} failed to apply", .0)] 18 + struct NodeError( 19 + Name, 20 + #[source] 21 + #[diagnostic_source] 22 + HiveLibError, 23 + ); 24 + 25 + #[derive(Debug, Error, Diagnostic)] 26 + #[error("{} node(s) failed to apply.", .0.len())] 27 + struct NodeErrors(#[related] Vec<NodeError>); 28 + 16 29 #[instrument(skip_all, fields(goal = %args.goal, on = %args.on.iter().join(", ")))] 17 30 pub async fn apply( 18 31 hive: &mut Hive, 19 32 args: ApplyArgs, 20 33 path: PathBuf, 21 34 modifiers: SubCommandModifiers, 22 - ) -> Result<(), anyhow::Error> { 35 + ) -> Result<()> { 23 36 let header_span = Span::current(); 24 37 header_span.pb_set_style(&ProgressStyle::default_bar()); 25 38 header_span.pb_set_length(1); ··· 97 110 std::mem::drop(header_span); 98 111 99 112 if !errors.is_empty() { 100 - return Err(anyhow!( 101 - "{} node(s) failed to apply. {}", 102 - errors.len(), 113 + return Err(NodeErrors( 103 114 errors 104 - .iter() 105 - .fold(String::new(), |mut output, (name, error)| { 106 - let _ = write!(output, "\n\n{name}: {error}"); 107 - output 108 - }) 109 - )); 115 + .into_iter() 116 + .map(|(name, error)| NodeError(name.clone(), error)) 117 + .collect(), 118 + ) 119 + .into()); 110 120 } 111 121 112 122 Ok(())
+1 -1
wire/cli/src/cli.rs
··· 141 141 } 142 142 143 143 impl TryFrom<Goal> for HiveGoal { 144 - type Error = anyhow::Error; 144 + type Error = miette::Error; 145 145 146 146 fn try_from(value: Goal) -> Result<Self, Self::Error> { 147 147 match value {
+4 -3
wire/cli/src/main.rs
··· 2 2 #![allow(clippy::missing_panics_doc)] 3 3 use crate::cli::Cli; 4 4 use crate::cli::ToSubCommandModifiers; 5 - use anyhow::Ok; 6 5 use clap::CommandFactory; 7 6 use clap::Parser; 8 7 use clap_complete::generate; 9 8 use clap_verbosity_flag::{Verbosity, WarnLevel}; 10 9 use indicatif::style::ProgressStyle; 11 10 use lib::hive::Hive; 11 + use miette::IntoDiagnostic; 12 + use miette::Result; 12 13 use tracing::warn; 13 14 use tracing_indicatif::IndicatifLayer; 14 15 use tracing_log::AsTrace; ··· 27 28 static ALLOC: dhat::Alloc = dhat::Alloc; 28 29 29 30 #[tokio::main] 30 - async fn main() -> Result<(), anyhow::Error> { 31 + async fn main() -> Result<()> { 31 32 #[cfg(feature = "dhat-heap")] 32 33 let _profiler = dhat::Profiler::new_heap(); 33 34 ··· 56 57 cli::Commands::Inspect { online: _, json } => println!("{}", { 57 58 let hive = Hive::new_from_path(args.path.as_path(), modifiers).await?; 58 59 if json { 59 - serde_json::to_string(&hive)? 60 + serde_json::to_string(&hive).into_diagnostic()? 60 61 } else { 61 62 warn!("use --json to output something scripting suitable"); 62 63 format!("{hive:#?}")
+7
wire/lib/Cargo.toml
··· 25 25 gethostname = "1.0.2" 26 26 async-trait = "0.1.88" 27 27 nix = { workspace = true } 28 + miette = { workspace = true } 28 29 29 30 [dev-dependencies] 30 31 tempdir = "0.3" 32 + 33 + [build-dependencies] 34 + miette = { workspace = true } 35 + syn = "2.0.104" 36 + proc-macro2 = "1.0.95" 37 + itertools = "0.14.0"
+202
wire/lib/build.rs
··· 1 + use miette::{Context, IntoDiagnostic as _, Result, miette}; 2 + use std::{ 3 + env, 4 + fmt::{self, Display, Formatter}, 5 + fs::{self}, 6 + path::Path, 7 + }; 8 + 9 + use itertools::Itertools; 10 + use proc_macro2::TokenTree; 11 + use syn::{Expr, Item, ItemEnum, Lit, Meta, MetaList, MetaNameValue, parse_file}; 12 + 13 + macro_rules! p { 14 + ($($tokens: tt)*) => { 15 + println!("cargo::warning={}", format!($($tokens)*)) 16 + } 17 + } 18 + 19 + #[derive(Debug)] 20 + struct DerviedError { 21 + code: Option<String>, 22 + help: Option<String>, 23 + message: Option<String>, 24 + doc_string: String, 25 + } 26 + 27 + impl Display for DerviedError { 28 + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 29 + write!( 30 + f, 31 + "## `{code}` {{#{code}}} 32 + 33 + {doc} 34 + {message} 35 + {help}", 36 + doc = self.doc_string, 37 + code = self.code.as_ref().unwrap(), 38 + help = match &self.help { 39 + Some(help) => format!( 40 + " 41 + ::: tip HELP 42 + {help} 43 + :::" 44 + ), 45 + None => "".to_string(), 46 + }, 47 + message = match &self.message { 48 + Some(message) => format!( 49 + " 50 + ```txt [message] 51 + {message} 52 + ```" 53 + ), 54 + None => "".to_string(), 55 + } 56 + ) 57 + } 58 + } 59 + 60 + impl DerviedError { 61 + fn get_error(&mut self, list: &MetaList) -> Result<(), miette::Error> { 62 + if list.path.segments.last().unwrap().ident != "error" { 63 + return Err(miette!("Not an error")); 64 + } 65 + 66 + self.message = Some( 67 + list.tokens 68 + .clone() 69 + .into_iter() 70 + .filter(|tok| matches!(tok, TokenTree::Literal(tok) if tok.to_string().starts_with("\""))) 71 + .map(|tok| tok.to_string()) 72 + .join(""), 73 + ); 74 + 75 + Err(miette!("No error msg found")) 76 + } 77 + 78 + fn update_diagnostic(&mut self, list: &MetaList) -> Result<(), miette::Error> { 79 + if list.path.segments.last().unwrap().ident != "diagnostic" { 80 + return Err(miette!("Not a diagnostic")); 81 + } 82 + 83 + let vec: Vec<_> = list.tokens.clone().into_iter().collect(); 84 + 85 + // Find `diagnostic(code(x::y::z))` 86 + let code: Option<String> = if let Some((_, TokenTree::Group(group))) = 87 + vec.iter().tuple_windows().find(|(ident, group)| { 88 + matches!(ident, TokenTree::Ident(ident) if ident == "code") 89 + && matches!(group, TokenTree::Group(..)) 90 + }) { 91 + Some(group.stream().to_string().replace(" ", "")) 92 + } else { 93 + None 94 + }; 95 + 96 + // Find `diagnostic(help("hi"))` 97 + let help: Option<String> = if let Some((_, TokenTree::Group(group))) = 98 + vec.iter().tuple_windows().find(|(ident, group)| { 99 + matches!(ident, TokenTree::Ident(ident) if ident == "help") 100 + && matches!(group, TokenTree::Group(..)) 101 + }) { 102 + Some(group.stream().to_string()) 103 + } else { 104 + None 105 + }; 106 + 107 + if let Some(code) = code { 108 + self.code = Some(code); 109 + self.help = help; 110 + return Ok(()); 111 + } 112 + 113 + Err(miette!("Had no code.")) 114 + } 115 + 116 + fn update_from_list(&mut self, list: MetaList) { 117 + let _ = self.get_error(&list); 118 + let _ = self.update_diagnostic(&list); 119 + } 120 + 121 + fn update_from_namevalue(&mut self, list: MetaNameValue) -> Result<(), miette::Error> { 122 + if list.path.segments.last().unwrap().ident != "doc" { 123 + return Err(miette!("Not a doc string")); 124 + } 125 + 126 + if let Expr::Lit(lit) = list.value { 127 + if let Lit::Str(str) = lit.lit { 128 + self.doc_string 129 + .push_str(&format!("{}\n\n", &str.value()[1..])); 130 + } 131 + } 132 + 133 + Ok(()) 134 + } 135 + } 136 + 137 + fn main() -> Result<()> { 138 + println!("cargo:rerun-if-changed=src/errors.rs"); 139 + 140 + let manifest_dir = env::var("CARGO_MANIFEST_DIR").into_diagnostic()?; 141 + let md_out_dir = if let Ok(path) = env::var("DIAGNOSTICS_MD_OUTPUT") { 142 + path 143 + } else { 144 + return Ok(()); 145 + }; 146 + 147 + let src_path = Path::new(&manifest_dir).join("src/errors.rs"); 148 + let src = fs::read_to_string(&src_path) 149 + .into_diagnostic() 150 + .wrap_err("reading errors.rs")?; 151 + 152 + let syntax_tree = parse_file(&src) 153 + .into_diagnostic() 154 + .wrap_err("parsing errors.rs")?; 155 + let mut entries: Vec<DerviedError> = Vec::new(); 156 + 157 + for item in &syntax_tree.items { 158 + if let Item::Enum(ItemEnum { variants, .. }) = item { 159 + for variant in variants { 160 + let mut entry = DerviedError { 161 + code: None, 162 + help: None, 163 + message: None, 164 + doc_string: String::new(), 165 + }; 166 + 167 + for attribute in variant.attrs.clone() { 168 + match attribute.meta { 169 + Meta::List(list) => { 170 + entry.update_from_list(list); 171 + } 172 + Meta::NameValue(nv) => { 173 + let _ = entry.update_from_namevalue(nv); 174 + } 175 + _ => {} 176 + } 177 + } 178 + 179 + if entry.code.is_some() { 180 + entries.push(entry); 181 + } 182 + } 183 + } 184 + } 185 + 186 + fs::create_dir_all(Path::new(&md_out_dir)) 187 + .into_diagnostic() 188 + .wrap_err("creating target directory")?; 189 + fs::write( 190 + Path::new(&md_out_dir).join("DIAGNOSTICS.md"), 191 + entries.iter().map(|x| x.to_string()).join("\n\n"), 192 + ) 193 + .into_diagnostic() 194 + .wrap_err("writing DIAGNOSTICS.md")?; 195 + 196 + p!( 197 + "wrote to {:?}", 198 + Path::new(&md_out_dir).join("DIAGNOSTICS.md") 199 + ); 200 + 201 + Ok(()) 202 + }
+299
wire/lib/src/errors.rs
··· 1 + use std::{num::ParseIntError, path::PathBuf, process::ExitStatus}; 2 + 3 + use miette::{Diagnostic, SourceSpan}; 4 + use thiserror::Error; 5 + 6 + use crate::{ 7 + format_error_lines, 8 + hive::node::{Name, SwitchToConfigurationGoal}, 9 + nix_log::{NixLog, Trace}, 10 + }; 11 + 12 + #[cfg(debug_assertions)] 13 + const DOCS_URL: &str = "http://localhost:5173/reference/errors.html"; 14 + #[cfg(not(debug_assertions))] 15 + const DOCS_URL: &str = "https://wire.althaea.zone/reference/errors.html"; 16 + 17 + #[derive(Debug, Diagnostic, Error)] 18 + pub enum KeyError { 19 + #[diagnostic( 20 + code(wire::Key::File), 21 + url("{DOCS_URL}#{}", self.code().unwrap()) 22 + )] 23 + #[error("error reading file")] 24 + File(#[source] std::io::Error), 25 + 26 + #[diagnostic( 27 + code(wire::Key::SpawningCommand), 28 + help("Ensure wire has the correct $PATH for this command"), 29 + url("{DOCS_URL}#{}", self.code().unwrap()) 30 + )] 31 + #[error("error spawning key command")] 32 + CommandSpawnError { 33 + #[source] 34 + error: std::io::Error, 35 + 36 + #[source_code] 37 + command: String, 38 + 39 + #[label(primary, "Program ran")] 40 + command_span: Option<SourceSpan>, 41 + }, 42 + 43 + #[diagnostic( 44 + code(wire::Key::Resolving), 45 + url("{DOCS_URL}#{}", self.code().unwrap()) 46 + )] 47 + #[error("Error resolving key command child process")] 48 + CommandResolveError { 49 + #[source] 50 + error: std::io::Error, 51 + 52 + #[source_code] 53 + command: String, 54 + }, 55 + 56 + #[diagnostic( 57 + code(wire::Key::CommandExit), 58 + url("{DOCS_URL}#{}", self.code().unwrap()) 59 + )] 60 + #[error("key command failed with status {}: {}", .0,.1)] 61 + CommandError(ExitStatus, String), 62 + 63 + #[diagnostic( 64 + code(wire::Key::Empty), 65 + url("{DOCS_URL}#{}", self.code().unwrap()) 66 + )] 67 + #[error("Command list empty")] 68 + Empty, 69 + 70 + #[diagnostic( 71 + code(wire::Key::ParseKeyPermissions), 72 + help("Refer to the documentation for the format of key file permissions."), 73 + url("{DOCS_URL}#{}", self.code().unwrap()) 74 + )] 75 + #[error("Failed to parse key permissions")] 76 + ParseKeyPermissions(#[source] ParseIntError), 77 + } 78 + 79 + #[derive(Debug, Diagnostic, Error)] 80 + pub enum KeyAgentError { 81 + #[diagnostic( 82 + code(wire::KeyAgent::SpawningAgent), 83 + help("Please create an issue!"), 84 + url("{DOCS_URL}#{}", self.code().unwrap()) 85 + )] 86 + #[error("Error spawning key agent")] 87 + SpawningAgent(#[source] std::io::Error), 88 + 89 + #[diagnostic( 90 + code(wire::KeyAgent::Resolving), 91 + help("Please create an issue!"), 92 + url("{DOCS_URL}#{}", self.code().unwrap()) 93 + )] 94 + #[error("Error resolving key agent child process")] 95 + ResolvingError(#[source] std::io::Error), 96 + 97 + #[diagnostic( 98 + code(wire::KeyAgent::Fail), 99 + help("If you suspect the reason is wire's fault, please create an issue!"), 100 + url("{DOCS_URL}#{}", self.code().unwrap()) 101 + )] 102 + #[error("failed to push keys (last 20 lines):\n{lines}", lines = format_error_lines(.1))] 103 + AgentFailed(Name, Vec<String>), 104 + } 105 + 106 + #[derive(Debug, Diagnostic, Error)] 107 + pub enum ActivationError { 108 + #[diagnostic( 109 + code(wire::Activation::SwitchToConfiguration), 110 + url("{DOCS_URL}#{}", self.code().unwrap()) 111 + )] 112 + #[error("failed to run switch-to-configuration {0} on node {1} (last 20 lines):\n{lines}", lines = format_error_lines(.2))] 113 + SwitchToConfigurationError(SwitchToConfigurationGoal, Name, Vec<String>), 114 + 115 + #[diagnostic( 116 + code(wire::Activation::Elevate), 117 + url("{DOCS_URL}#{}", self.code().unwrap()) 118 + )] 119 + #[error("failed to elevate")] 120 + FailedToElevate(#[source] std::io::Error), 121 + 122 + #[diagnostic( 123 + code(wire::Activation::NixEnv), 124 + url("{DOCS_URL}#{}", self.code().unwrap()) 125 + )] 126 + #[error("failed to run nix-env on node {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] 127 + NixEnvError(Name, Vec<String>), 128 + } 129 + 130 + #[derive(Debug, Diagnostic, Error)] 131 + pub enum NetworkError { 132 + #[diagnostic( 133 + code(wire::Network::HostUnreachable), 134 + help( 135 + "If you failed due to a fault in DNS, note that a node can have multiple targets defined." 136 + ), 137 + url("{DOCS_URL}#{}", self.code().unwrap()) 138 + )] 139 + #[error("Cannot reach host {0}")] 140 + HostUnreachable(String), 141 + 142 + #[diagnostic( 143 + code(wire::Network::HostUnreachableAfterReboot), 144 + url("{DOCS_URL}#{}", self.code().unwrap()) 145 + )] 146 + #[error("Cannot reach host {0} after reboot")] 147 + HostUnreachableAfterReboot(String), 148 + 149 + #[diagnostic( 150 + code(wire::Network::HostsExhausted), 151 + url("{DOCS_URL}#{}", self.code().unwrap()) 152 + )] 153 + #[error("Ran out of contactable hosts")] 154 + HostsExhausted, 155 + } 156 + 157 + #[derive(Debug, Diagnostic, Error)] 158 + pub enum HiveInitializationError { 159 + #[diagnostic( 160 + code(wire::HiveInit::NoHiveFound), 161 + help( 162 + "Double check the path is correct. You can adjust the hive path with `--path` when the hive lies outside of the CWD." 163 + ), 164 + url("{DOCS_URL}#{}", self.code().unwrap()) 165 + )] 166 + #[error("No hive could be found in {}", .0.display())] 167 + NoHiveFound(PathBuf), 168 + 169 + #[diagnostic( 170 + code(wire::HiveInit::NixEval), 171 + help("Check your hive is syntactically valid."), 172 + url("{DOCS_URL}#{}", self.code().unwrap()) 173 + )] 174 + #[error("failed to evaluate your hive! last 20 lines:\n{}", format_error_lines(.0))] 175 + NixEvalError(Vec<String>), 176 + 177 + #[diagnostic( 178 + code(wire::HiveInit::Parse), 179 + help("Please create an issue!"), 180 + url("{DOCS_URL}#{}", self.code().unwrap()) 181 + )] 182 + #[error("Failed to parse internal wire json.")] 183 + ParseEvaluateError(#[source] serde_json::Error), 184 + 185 + #[diagnostic( 186 + code(wire::HiveInit::NodeDoesNotExist), 187 + help("Please create an issue!"), 188 + url("{DOCS_URL}#{}", self.code().unwrap()) 189 + )] 190 + #[error("node {0} not exist in hive")] 191 + NodeDoesNotExist(String), 192 + } 193 + 194 + #[derive(Debug, Diagnostic, Error)] 195 + pub enum NixChildError { 196 + #[diagnostic( 197 + code(wire::NixChild::JoiningTasks), 198 + help("This should never happen, please create an issue!"), 199 + url("{DOCS_URL}#{}", self.code().unwrap()) 200 + 201 + )] 202 + #[error("Could not join nix logging task")] 203 + JoinError(#[source] tokio::task::JoinError), 204 + 205 + #[diagnostic( 206 + code(wire::NixChild::NoHandle), 207 + help("This should never happen, please create an issue!"), 208 + url("{DOCS_URL}#{}", self.code().unwrap()) 209 + )] 210 + #[error("There was no handle to io on the child process")] 211 + NoHandle, 212 + 213 + #[diagnostic( 214 + code(wire::NixChild::SpawnFailed), 215 + help("Please run wire under a host with nix installed."), 216 + url("{DOCS_URL}#{}", self.code().unwrap()) 217 + )] 218 + #[error("failed to execute nix")] 219 + SpawnFailed(#[source] tokio::io::Error), 220 + 221 + #[diagnostic( 222 + code(wire::NixChild::Resolving), 223 + url("{DOCS_URL}#{}", self.code().unwrap()) 224 + )] 225 + #[error("Error resolving nix child process")] 226 + ResolveError(#[source] std::io::Error), 227 + } 228 + 229 + #[derive(Debug, Diagnostic, Error)] 230 + pub enum HiveLibError { 231 + #[error(transparent)] 232 + #[diagnostic(transparent)] 233 + HiveInitializationError(HiveInitializationError), 234 + 235 + #[error(transparent)] 236 + #[diagnostic(transparent)] 237 + NetworkError(NetworkError), 238 + 239 + #[error(transparent)] 240 + #[diagnostic(transparent)] 241 + ActivationError(ActivationError), 242 + 243 + #[error("Failed to apply key {}", .0)] 244 + KeyError( 245 + String, 246 + #[source] 247 + #[diagnostic_source] 248 + KeyError, 249 + ), 250 + 251 + #[error("Wire key-agent failed")] 252 + KeyAgentError( 253 + #[source] 254 + #[diagnostic_source] 255 + KeyAgentError, 256 + ), 257 + 258 + #[error(transparent)] 259 + #[diagnostic(transparent)] 260 + NixChildError(NixChildError), 261 + 262 + #[diagnostic( 263 + code(wire::EvaluateNode), 264 + url("{DOCS_URL}#{}", self.code().unwrap()) 265 + )] 266 + #[error( 267 + "failed to evaluate node {0} (filtered logs, run with -vvv to see all):\n{log}", 268 + log = .1.iter().filter(|l| l.is_error()).map(std::string::ToString::to_string).collect::<Vec<String>>().join("\n")) 269 + ] 270 + NixEvalInternalError(Name, Vec<NixLog>), 271 + 272 + #[diagnostic( 273 + code(wire::BuildNode), 274 + url("{DOCS_URL}#{}", self.code().unwrap()) 275 + )] 276 + #[error("failed to build node {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] 277 + NixBuildError(Name, Vec<String>), 278 + 279 + #[diagnostic( 280 + code(wire::CopyPath), 281 + url("{DOCS_URL}#{}", self.code().unwrap()) 282 + )] 283 + #[error( 284 + "failed to copy path {path} to node {name} (filtered logs, run with -vvv to see all):\n{log}", 285 + log = logs.iter().filter(|l| l.is_error()).map(std::string::ToString::to_string).collect::<Vec<String>>().join("\n")) 286 + ] 287 + NixCopyError { 288 + name: Name, 289 + path: String, 290 + logs: Vec<NixLog>, 291 + }, 292 + 293 + #[diagnostic( 294 + code(wire::BufferOperation), 295 + url("{DOCS_URL}#{}", self.code().unwrap()) 296 + )] 297 + #[error("an operation failed in regards to buffers")] 298 + BufferOperationError(#[source] tokio::io::Error), 299 + }
+23 -11
wire/lib/src/hive/mod.rs
··· 7 7 use std::sync::Arc; 8 8 use tracing::{debug, error, info, instrument, trace}; 9 9 10 + use crate::errors::{HiveInitializationError, NixChildError}; 10 11 use crate::nix::{EvalGoal, get_eval_command}; 11 12 use crate::{HiveLibError, SubCommandModifiers}; 12 13 pub mod node; ··· 49 50 let command = get_eval_command(path, &EvalGoal::Inspect, modifiers)? 50 51 .output() 51 52 .await 52 - .map_err(HiveLibError::NixExecError)?; 53 + .map_err(|err| HiveLibError::NixChildError(NixChildError::ResolveError(err)))?; 53 54 54 55 let stdout = String::from_utf8_lossy(&command.stdout); 55 56 let stderr = String::from_utf8_lossy(&command.stderr); ··· 57 58 debug!("Output of nix eval: {stdout}"); 58 59 59 60 if command.status.success() { 60 - let hive: Hive = 61 - serde_json::from_str(&stdout).map_err(HiveLibError::ParseEvaluateError)?; 61 + let hive: Hive = serde_json::from_str(&stdout).map_err(|err| { 62 + HiveLibError::HiveInitializationError(HiveInitializationError::ParseEvaluateError( 63 + err, 64 + )) 65 + })?; 62 66 63 67 return Ok(hive); 64 68 } 65 69 66 - Err(HiveLibError::NixEvalError( 67 - stderr 68 - .split('\n') 69 - .map(std::string::ToString::to_string) 70 - .collect(), 70 + Err(HiveLibError::HiveInitializationError( 71 + HiveInitializationError::NixEvalError( 72 + stderr 73 + .split('\n') 74 + .map(std::string::ToString::to_string) 75 + .collect(), 76 + ), 71 77 )) 72 78 } 73 79 ··· 80 86 81 87 self.nodes 82 88 .get_mut(&Name(Arc::from(node.clone()))) 83 - .ok_or(HiveLibError::NodeDoesNotExist(node.to_string()))? 89 + .ok_or(HiveLibError::HiveInitializationError( 90 + HiveInitializationError::NodeDoesNotExist(node.to_string()), 91 + ))? 84 92 .build_remotely = false; 85 93 } 86 94 ··· 234 242 235 243 assert!(matches!( 236 244 Hive::new_from_path(&path, SubCommandModifiers::default()).await, 237 - Err(HiveLibError::NixEvalError(..)) 245 + Err(HiveLibError::HiveInitializationError( 246 + HiveInitializationError::NixEvalError(..) 247 + )) 238 248 )); 239 249 } 240 250 ··· 244 254 245 255 assert!(matches!( 246 256 Hive::new_from_path(&path, SubCommandModifiers::default()).await, 247 - Err(HiveLibError::NixEvalError(..)) 257 + Err(HiveLibError::HiveInitializationError( 258 + HiveInitializationError::NixEvalError(..) 259 + )) 248 260 )); 249 261 } 250 262 }
+9 -4
wire/lib/src/hive/node.rs
··· 10 10 use tracing_indicatif::span_ext::IndicatifSpanExt; 11 11 12 12 use crate::SubCommandModifiers; 13 + use crate::errors::NetworkError; 13 14 use crate::hive::steps::keys::{Key, KeysStep, PushKeyAgentStep, UploadKeyAt}; 14 15 use crate::hive::steps::ping::PingStep; 15 16 use crate::nix::StreamTracing; ··· 46 47 pub fn get_preffered_host(&self) -> Result<&Arc<str>, HiveLibError> { 47 48 self.hosts 48 49 .get(self.current_host) 49 - .ok_or(HiveLibError::HostsExhausted) 50 + .ok_or(HiveLibError::NetworkError(NetworkError::HostsExhausted)) 50 51 } 51 52 52 53 pub fn host_failed(&mut self) { ··· 129 130 return Ok(()); 130 131 } 131 132 132 - Err(HiveLibError::HostUnreachable( 133 + Err(HiveLibError::NetworkError(NetworkError::HostUnreachable( 133 134 self.target.get_preffered_host()?.to_string(), 134 - )) 135 + ))) 135 136 } 136 137 } 137 138 ··· 273 274 let (status, _stdout, stderr_vec) = command.execute(true).in_current_span().await?; 274 275 275 276 if !status.success() { 276 - return Err(HiveLibError::NixCopyError(name.clone(), stderr_vec)); 277 + return Err(HiveLibError::NixCopyError { 278 + name: name.clone(), 279 + logs: stderr_vec, 280 + path: push.to_string(), 281 + }); 277 282 } 278 283 279 284 Ok(())
+19 -17
wire/lib/src/hive/steps/activate.rs
··· 7 7 8 8 use crate::{ 9 9 HiveLibError, create_ssh_command, 10 + errors::{ActivationError, NetworkError}, 10 11 hive::node::{Context, ExecuteStep, Goal, SwitchToConfigurationGoal, should_apply_locally}, 11 12 nix::StreamTracing, 12 13 }; ··· 19 20 } 20 21 } 21 22 22 - pub(crate) fn get_elevation(reason: &str) -> Result<Output, HiveLibError> { 23 + pub(crate) fn get_elevation(reason: &str) -> Result<Output, ActivationError> { 23 24 info!("Attempting to elevate for {reason}."); 24 25 suspend_tracing_indicatif(|| { 25 26 let mut command = std::process::Command::new("sudo"); 26 27 command.arg("-v").output() 27 28 }) 28 - .map_err(HiveLibError::FailedToElevate) 29 + .map_err(ActivationError::FailedToElevate) 29 30 } 30 31 31 32 pub async fn wait_for_ping(ctx: &Context<'_>) -> Result<(), HiveLibError> { ··· 46 47 } 47 48 } 48 49 49 - Err(HiveLibError::HostUnreachable( 50 + Err(HiveLibError::NetworkError(NetworkError::HostUnreachable( 50 51 ctx.node.target.get_preffered_host()?.to_string(), 51 - )) 52 + ))) 52 53 } 53 54 54 55 #[async_trait] ··· 75 76 if should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { 76 77 // Refresh sudo timeout 77 78 warn!("Running nix-env ON THIS MACHINE for node {0}", ctx.name); 78 - get_elevation("nix-env")?; 79 + get_elevation("nix-env").map_err(HiveLibError::ActivationError)?; 79 80 let mut command = Command::new("sudo"); 80 81 command.arg("nix-env"); 81 82 command ··· 96 97 .filter(|s| !s.is_empty()) 97 98 .collect(); 98 99 99 - return Err(HiveLibError::NixEnvError(ctx.name.clone(), stderr)); 100 + return Err(HiveLibError::ActivationError(ActivationError::NixEnvError( 101 + ctx.name.clone(), 102 + stderr, 103 + ))); 100 104 } 101 105 102 106 info!("Set system profile"); ··· 113 117 "Running switch-to-configuration {goal:?} ON THIS MACHINE for node {0}", 114 118 ctx.name 115 119 ); 116 - get_elevation("switch-to-configuration")?; 120 + get_elevation("switch-to-configuration").map_err(HiveLibError::ActivationError)?; 117 121 let mut command = Command::new("sudo"); 118 122 command.arg(cmd); 119 123 command ··· 167 171 host = ctx.node.target.get_preffered_host()? 168 172 ); 169 173 170 - return Err(HiveLibError::HostUnreachableAfterReboot( 171 - ctx.node.target.get_preffered_host()?.to_string(), 174 + return Err(HiveLibError::NetworkError( 175 + NetworkError::HostUnreachableAfterReboot( 176 + ctx.node.target.get_preffered_host()?.to_string(), 177 + ), 172 178 )); 173 179 } 174 180 ··· 188 194 if matches!(goal, SwitchToConfigurationGoal::DryActivate) 189 195 || should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) 190 196 { 191 - return Err(HiveLibError::SwitchToConfigurationError( 192 - *goal, 193 - ctx.name.clone(), 194 - stderr, 197 + return Err(HiveLibError::ActivationError( 198 + ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), stderr), 195 199 )); 196 200 } 197 201 ··· 205 209 host = ctx.node.target.get_preffered_host()? 206 210 ); 207 211 208 - return Err(HiveLibError::SwitchToConfigurationError( 209 - *goal, 210 - ctx.name.clone(), 211 - stderr, 212 + return Err(HiveLibError::ActivationError( 213 + ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), stderr), 212 214 )); 213 215 } 214 216 }
+1 -1
wire/lib/src/hive/steps/evaluate.rs
··· 48 48 return Ok(()); 49 49 } 50 50 51 - Err(HiveLibError::NixEvalInteralError(ctx.name.clone(), stderr)) 51 + Err(HiveLibError::NixEvalInternalError(ctx.name.clone(), stderr)) 52 52 } 53 53 }
+23 -31
wire/lib/src/hive/steps/keys.rs
··· 5 5 use std::env; 6 6 use std::fmt::Display; 7 7 use std::io::Cursor; 8 + use std::path::PathBuf; 8 9 use std::pin::Pin; 9 - use std::process::{ExitStatus, Stdio}; 10 + use std::process::Stdio; 10 11 use std::str::from_utf8; 11 - use std::{num::ParseIntError, path::PathBuf}; 12 - use thiserror::Error; 13 12 use tokio::io::{AsyncReadExt as _, AsyncWriteExt}; 14 13 use tokio::process::Command; 15 14 use tokio::{fs::File, io::AsyncRead}; 16 15 use tracing::{debug, info, trace, warn}; 17 16 17 + use crate::errors::{KeyAgentError, KeyError}; 18 18 use crate::hive::node::{ 19 19 Context, ExecuteStep, Goal, Push, SwitchToConfigurationGoal, push, should_apply_locally, 20 20 }; 21 21 use crate::hive::steps::activate::get_elevation; 22 22 use crate::{HiveLibError, create_ssh_command}; 23 - 24 - #[derive(Debug, Error)] 25 - pub enum KeyError { 26 - #[error("error reading file")] 27 - File(#[source] std::io::Error), 28 - 29 - #[error("error spawning command")] 30 - CommandSpawnError(#[source] std::io::Error), 31 - 32 - #[error("key command failed with status {}: {}", .0,.1)] 33 - CommandError(ExitStatus, String), 34 - 35 - #[error("Command list empty")] 36 - Empty, 37 - 38 - #[error("Failed to parse key permissions")] 39 - ParseKeyPermissions(#[source] ParseIntError), 40 - } 41 23 42 24 #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] 43 25 #[serde(tag = "t", content = "c")] ··· 105 87 .stdout(Stdio::piped()) 106 88 .stderr(Stdio::piped()) 107 89 .spawn() 108 - .map_err(KeyError::CommandSpawnError)? 90 + .map_err(|err| KeyError::CommandSpawnError { 91 + error: err, 92 + command: args.join(" "), 93 + command_span: Some((0..args.first().unwrap().len()).into()), 94 + })? 109 95 .wait_with_output() 110 96 .await 111 - .map_err(KeyError::CommandSpawnError)?; 97 + .map_err(|err| KeyError::CommandResolveError { 98 + error: err, 99 + command: args.join(" "), 100 + })?; 112 101 113 102 if output.status.success() { 114 103 return Ok(Box::pin(Cursor::new(output.stdout))); ··· 214 203 self.filter == UploadKeyAt::NoFilter 215 204 || (self.filter != UploadKeyAt::NoFilter && key.upload_at != self.filter) 216 205 }) 217 - .map(|key| async move { process_key(key).await }); 206 + .map(|key| async move { 207 + process_key(key) 208 + .await 209 + .map_err(|err| HiveLibError::KeyError(key.name.clone(), err)) 210 + }); 218 211 219 212 let (keys, bufs): (Vec<key_agent::keys::Key>, Vec<Vec<u8>>) = join_all(futures) 220 213 .await 221 214 .into_iter() 222 - .collect::<Result<Vec<_>, KeyError>>() 223 - .map_err(HiveLibError::KeyError)? 215 + .collect::<Result<Vec<_>, HiveLibError>>()? 224 216 .into_iter() 225 217 .unzip(); 226 218 ··· 233 225 let mut command = 234 226 if should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { 235 227 warn!("Placing keys locally for node {0}", ctx.name); 236 - get_elevation("wire key agent")?; 228 + get_elevation("wire key agent").map_err(HiveLibError::ActivationError)?; 237 229 Command::new("sudo") 238 230 } else { 239 231 create_ssh_command(&ctx.node.target, true)? ··· 248 240 .stderr(Stdio::piped()) 249 241 .stdin(Stdio::piped()) 250 242 .spawn() 251 - .map_err(HiveLibError::SpawnFailed)?; 243 + .map_err(|err| HiveLibError::KeyAgentError(KeyAgentError::SpawningAgent(err)))?; 252 244 253 245 // take() stdin so it will be dropped out of block 254 246 if let Some(mut stdin) = child.stdin.take() { ··· 260 252 let output = child 261 253 .wait_with_output() 262 254 .await 263 - .map_err(HiveLibError::SpawnFailed)?; 255 + .map_err(|err| HiveLibError::KeyAgentError(KeyAgentError::ResolvingError(err)))?; 264 256 265 257 if output.status.success() { 266 258 info!("Successfully pushed keys to {}", ctx.name); ··· 271 263 272 264 let stderr = String::from_utf8_lossy(&output.stderr); 273 265 274 - Err(HiveLibError::KeyCommandError( 266 + Err(HiveLibError::KeyAgentError(KeyAgentError::AgentFailed( 275 267 ctx.name.clone(), 276 268 stderr 277 269 .split('\n') 278 270 .map(std::string::ToString::to_string) 279 271 .collect(), 280 - )) 272 + ))) 281 273 } 282 274 } 283 275
+6 -80
wire/lib/src/lib.rs
··· 4 4 clippy::must_use_candidate, 5 5 clippy::missing_panics_doc 6 6 )] 7 - use hive::{ 8 - node::{Name, SwitchToConfigurationGoal, Target}, 9 - steps::keys::KeyError, 10 - }; 11 - use nix_log::{NixLog, Trace}; 12 - use std::path::PathBuf; 13 - use thiserror::Error; 14 - use tokio::{process::Command, task::JoinError}; 7 + use hive::node::Target; 8 + use tokio::process::Command; 9 + 10 + use crate::errors::HiveLibError; 15 11 16 12 pub mod hive; 17 13 mod nix; ··· 22 18 23 19 #[cfg(test)] 24 20 mod test_support; 21 + 22 + pub mod errors; 25 23 26 24 fn create_ssh_command(target: &Target, sudo: bool) -> Result<Command, HiveLibError> { 27 25 let mut command = Command::new("ssh"); ··· 47 45 .cloned() 48 46 .collect::<Vec<_>>() 49 47 .join("\n") 50 - } 51 - 52 - #[derive(Debug, Error)] 53 - pub enum HiveLibError { 54 - #[error("no hive could be found in {}", .0.display())] 55 - NoHiveFound(PathBuf), 56 - 57 - #[error("failed to execute nix command")] 58 - NixExecError(#[source] tokio::io::Error), 59 - 60 - #[error("failed to evaluate your hive! is it valid? (last 20 lines):\n{}", format_error_lines(.0))] 61 - NixEvalError(Vec<String>), 62 - 63 - #[error( 64 - "failed to evaluate node {0} (filtered logs, run with -vvv to see all):\n{log}", 65 - log = .1.iter().filter(|l| l.is_error()).map(std::string::ToString::to_string).collect::<Vec<String>>().join("\n")) 66 - ] 67 - NixEvalInteralError(Name, Vec<NixLog>), 68 - 69 - #[error( 70 - "failed to copy drv to node {0} (filtered logs, run with -vvv to see all):\n{log}", 71 - log = .1.iter().filter(|l| l.is_error()).map(std::string::ToString::to_string).collect::<Vec<String>>().join("\n")) 72 - ] 73 - NixCopyError(Name, Vec<NixLog>), 74 - 75 - #[error("failed to build node {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] 76 - NixBuildError(Name, Vec<String>), 77 - 78 - #[error("failed to run switch-to-configuration {0} on node {1} (last 20 lines):\n{lines}", lines = format_error_lines(.2))] 79 - SwitchToConfigurationError(SwitchToConfigurationGoal, Name, Vec<String>), 80 - 81 - #[error("failed to run nix-env on node {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] 82 - NixEnvError(Name, Vec<String>), 83 - 84 - #[error("failed to push keys to {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] 85 - KeyCommandError(Name, Vec<String>), 86 - 87 - #[error("failed to push a key")] 88 - KeyError(#[source] KeyError), 89 - 90 - #[error("node {0} not exist in hive")] 91 - NodeDoesNotExist(String), 92 - 93 - #[error("failed to execute command")] 94 - SpawnFailed(#[source] tokio::io::Error), 95 - 96 - #[error("failed to join task")] 97 - JoinError(#[source] JoinError), 98 - 99 - #[error("there was no handle to io on the child process")] 100 - NoHandle, 101 - 102 - #[error("failed to parse nix log \"{0}\"")] 103 - ParseLogError(String, #[source] serde_json::Error), 104 - 105 - #[error("failed to parse internal wire json. please create an issue!")] 106 - ParseEvaluateError(#[source] serde_json::Error), 107 - 108 - #[error("an operation failed in regards to buffers")] 109 - BufferOperationError(#[source] tokio::io::Error), 110 - 111 - #[error("failed to elevate")] 112 - FailedToElevate(#[source] std::io::Error), 113 - 114 - #[error("Cannot reach host {0}")] 115 - HostUnreachable(String), 116 - 117 - #[error("Cannot reach host {0} after reboot")] 118 - HostUnreachableAfterReboot(String), 119 - 120 - #[error("Ran out of contactable hosts")] 121 - HostsExhausted, 122 48 } 123 49 124 50 #[derive(Debug, Default, Clone, Copy)]
+23 -10
wire/lib/src/nix.rs
··· 7 7 use tracing::{Instrument, Span, error, info, trace}; 8 8 use tracing_indicatif::span_ext::IndicatifSpanExt; 9 9 10 + use crate::errors::{HiveInitializationError, NixChildError}; 10 11 use crate::hive::find_hive; 11 12 use crate::hive::node::Name; 12 13 use crate::nix_log::{Action, Internal, NixLog, Trace}; ··· 47 48 ) -> Result<tokio::process::Command, HiveLibError> { 48 49 assert!(check_nix_available(), "nix is not available on this system"); 49 50 50 - let canon_path = find_hive(&path.canonicalize().unwrap()) 51 - .ok_or(HiveLibError::NoHiveFound(path.to_path_buf()))?; 51 + let canon_path = 52 + find_hive(&path.canonicalize().unwrap()).ok_or(HiveLibError::HiveInitializationError( 53 + HiveInitializationError::NoHiveFound(path.to_path_buf()), 54 + ))?; 52 55 53 56 let mut command = tokio::process::Command::new("nix"); 54 57 command.args(["--extra-experimental-features", "nix-command"]); ··· 92 95 while let Some(line) = io_reader 93 96 .next_line() 94 97 .await 95 - .map_err(HiveLibError::SpawnFailed)? 98 + .map_err(|err| HiveLibError::NixChildError(NixChildError::SpawnFailed(err)))? 96 99 { 97 100 let log = serde_json::from_str::<Internal>(line.strip_prefix("@nix ").unwrap_or(&line)) 98 101 .map(NixLog::Internal) ··· 142 145 .stderr(std::process::Stdio::piped()) 143 146 .stdout(std::process::Stdio::piped()) 144 147 .spawn() 145 - .map_err(HiveLibError::SpawnFailed)?; 148 + .map_err(|err| HiveLibError::NixChildError(NixChildError::SpawnFailed(err)))?; 146 149 147 - let stdout_handle = child.stdout.take().ok_or(HiveLibError::NoHandle)?; 148 - let stderr_handle = child.stderr.take().ok_or(HiveLibError::NoHandle)?; 150 + let stdout_handle = child 151 + .stdout 152 + .take() 153 + .ok_or(HiveLibError::NixChildError(NixChildError::NoHandle))?; 154 + let stderr_handle = child 155 + .stderr 156 + .take() 157 + .ok_or(HiveLibError::NixChildError(NixChildError::NoHandle))?; 149 158 150 159 let stderr_task = tokio::spawn(handle_io(stderr_handle, log_stderr).in_current_span()); 151 160 let stdout_task = tokio::spawn(handle_io(stdout_handle, false)); 152 161 153 - let handle = 154 - tokio::spawn(async move { child.wait().await.map_err(HiveLibError::SpawnFailed) }); 162 + let handle = tokio::spawn(async move { 163 + child 164 + .wait() 165 + .await 166 + .map_err(|err| HiveLibError::NixChildError(NixChildError::SpawnFailed(err))) 167 + }); 155 168 156 - let (result, stdout, stderr) = 157 - tokio::try_join!(handle, stdout_task, stderr_task).map_err(HiveLibError::JoinError)?; 169 + let (result, stdout, stderr) = tokio::try_join!(handle, stdout_task, stderr_task) 170 + .map_err(|err| HiveLibError::NixChildError(NixChildError::JoinError(err)))?; 158 171 159 172 Ok((result?, stdout?, stderr?)) 160 173 }