ALPHA: wire is a tool to deploy nixos systems
wire.althaea.zone/
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}