ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/
at stable 167 lines 5.0 kB view raw
1use crate::hive::node::Step; 2use std::{assert_matches::debug_assert_matches, sync::Arc}; 3 4use tracing::{Instrument, Span, debug, error, event, instrument}; 5 6use crate::{ 7 EvalGoal, SubCommandModifiers, 8 commands::common::evaluate_hive_attribute, 9 errors::HiveLibError, 10 hive::{ 11 HiveLocation, 12 node::{Context, Derivation, ExecuteStep, Name}, 13 plan::NodePlan, 14 }, 15 status::STATUS, 16}; 17 18/// returns Err if the application should shut down. 19fn app_shutdown_guard(context: &Context) -> Result<(), HiveLibError> { 20 if context 21 .should_quit 22 .load(std::sync::atomic::Ordering::Relaxed) 23 { 24 return Err(HiveLibError::Sigint); 25 } 26 27 Ok(()) 28} 29 30/// Task that evaluates the node. 31#[instrument(skip_all, name = "eval")] 32async fn evaluate_task( 33 tx: tokio::sync::oneshot::Sender<Result<Derivation, HiveLibError>>, 34 hive_location: Arc<HiveLocation>, 35 name: Name, 36 modifiers: SubCommandModifiers, 37) { 38 let output = evaluate_hive_attribute(&hive_location, &EvalGoal::GetTopLevel(&name), modifiers) 39 .await 40 .and_then(|output| { 41 serde_json::from_str(&output).map_err(|e| { 42 HiveLibError::HiveInitialisationError( 43 crate::errors::HiveInitialisationError::ParseEvaluateError(e), 44 ) 45 }) 46 }); 47 48 debug!(output = ?output, done = true); 49 50 let _ = tx.send(output); 51} 52 53/// Iterates and executes the steps in the plan. 54/// Performs some optimisations such as greedily executing evaluation before 55/// other steps independent of evaluation's result. 56#[instrument(skip_all, fields(node = %plan.context.name))] 57pub async fn execute(mut plan: NodePlan) -> Result<(), HiveLibError> { 58 app_shutdown_guard(&plan.context)?; 59 60 let (tx, rx) = tokio::sync::oneshot::channel(); 61 plan.context.state.evaluation_rx = Some(rx); 62 63 // The name of this span should never be changed without updating 64 // `wire/cli/tracing_setup.rs` 65 debug_assert_matches!(Span::current().metadata().unwrap().name(), "execute"); 66 // This span should always have a `node` field by the same file 67 debug_assert!( 68 Span::current() 69 .metadata() 70 .unwrap() 71 .fields() 72 .field("node") 73 .is_some() 74 ); 75 76 if plan.greedy_evaluate { 77 tokio::spawn( 78 evaluate_task( 79 tx, 80 plan.context.hive_location.clone(), 81 plan.context.name.clone(), 82 plan.context.modifiers, 83 ) 84 .in_current_span(), 85 ); 86 } 87 88 let length = plan.steps.len(); 89 90 for (position, step) in plan.steps.iter().enumerate() { 91 app_shutdown_guard(&plan.context)?; 92 93 event!( 94 tracing::Level::INFO, 95 step = step.to_string(), 96 progress = format!("{}/{length}", position + 1) 97 ); 98 99 STATUS 100 .lock() 101 .set_node_step(&plan.context.name, step.to_string()); 102 103 if let Err(err) = step.execute(&mut plan.context).await.inspect_err(|_| { 104 error!("Failed to execute `{step}`"); 105 }) { 106 if matches!(step, Step::Ping(..)) && plan.ignore_failed_ping { 107 return Ok(()); 108 } 109 110 STATUS.lock().mark_node_failed(&plan.context.name); 111 112 return Err(err); 113 } 114 } 115 116 STATUS.lock().mark_node_succeeded(&plan.context.name); 117 118 Ok(()) 119} 120 121#[cfg(test)] 122mod tests { 123 use crate::{ 124 SubCommandModifiers, 125 errors::HiveLibError, 126 function_name, get_test_path, 127 hive::{ 128 executor::execute, 129 node::{ApplyGoal, HandleUnreachable, Name, Node, SwitchToConfigurationGoal}, 130 plan::{ApplyGoalArgs, Goal, plan_for_node}, 131 }, 132 location, 133 }; 134 use std::{assert_matches::assert_matches, path::PathBuf}; 135 use std::{ 136 env, 137 sync::{Arc, atomic::AtomicBool}, 138 }; 139 140 #[tokio::test] 141 async fn plan_executor_quits_sigint() { 142 let location = location!(get_test_path!()); 143 let node = Node::default(); 144 let name = &Name(function_name!().into()); 145 let should_quit = Arc::new(AtomicBool::new(true)); 146 let plan = plan_for_node( 147 &node.clone(), 148 name.clone(), 149 &Goal::Apply(ApplyGoalArgs { 150 goal: ApplyGoal::SwitchToConfiguration(SwitchToConfigurationGoal::Switch), 151 should_apply_locally: true, 152 no_keys: true, 153 substitute_on_destination: true, 154 reboot: false, 155 host_platform: "x86_64-linux".into(), 156 handle_unreachable: HandleUnreachable::default(), 157 }), 158 location.clone().into(), 159 &SubCommandModifiers::default(), 160 should_quit.clone(), 161 ); 162 163 let status = execute(plan).await; 164 165 assert_matches!(status, Err(HiveLibError::Sigint)); 166 } 167}