1use std::collections::BTreeMap;
7use std::path::PathBuf;
8use std::time::{Duration, Instant};
9
10use serde::Serialize;
11
12use super::error::EngineError;
13use super::progress::{NullProgress, ProgressSink, StepProgress, StepProgressEvent, epoch_ms};
14
15use crate::def::{DefError, StackDef};
16use crate::state::{InstanceStatus, Store};
17use crate::substrate::{Observation, StepContext, Substrate};
18
19pub struct Engine<'a> {
20 pub store: &'a Store,
21 pub substrate: &'a dyn Substrate,
22}
23
24impl std::fmt::Debug for Engine<'_> {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("Engine")
27 .field("substrate", &self.substrate.name())
28 .finish_non_exhaustive()
29 }
30}
31
32pub struct UpRequest<'a> {
33 pub instance: &'a str,
34 pub definition_text: &'a str,
36 pub def: &'a StackDef,
37 pub source_overrides: BTreeMap<String, String>,
38 pub dirty: bool,
40 pub definition_dir: String,
42 pub lease: Option<Duration>,
44 pub progress: Option<&'a mut dyn ProgressSink>,
46}
47
48impl std::fmt::Debug for UpRequest<'_> {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("UpRequest")
51 .field("instance", &self.instance)
52 .field("definition_dir", &self.definition_dir)
53 .field("lease", &self.lease)
54 .field("progress", &self.progress.is_some())
55 .finish_non_exhaustive()
56 }
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
60pub struct StepTiming {
61 pub id: String,
62 pub duration_ms: u64,
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq)]
66pub struct UpOutcome {
67 pub executed: Vec<String>,
68 pub skipped: Vec<String>,
69 pub duration_ms: u64,
70 pub steps: Vec<StepTiming>,
71}
72
73#[derive(Debug, PartialEq, Eq)]
74pub enum DownOutcome {
75 Destroyed,
77 AlreadyDown,
79}
80
81impl Engine<'_> {
82 pub async fn up(&self, request: UpRequest<'_>) -> Result<UpOutcome, EngineError> {
85 if !crate::types::dns_safe(request.instance) {
86 return Err(DefError::NameInvalid {
87 kind: "instance",
88 name: request.instance.to_owned(),
89 }
90 .into());
91 }
92 request.def.validate_for_substrate(self.substrate.name())?;
93 self.substrate
94 .validate_definition(request.def)
95 .map_err(|fault| EngineError::SubstrateValidation {
96 substrate: self.substrate.name().to_owned(),
97 fault,
98 })?;
99 if !request.source_overrides.is_empty() && !self.substrate.supports_source_override() {
100 return Err(EngineError::SourceOverrideUnsupported {
101 substrate: self.substrate.name().to_owned(),
102 });
103 }
104 if !request.source_overrides.is_empty() {
105 self.check_source_override_collisions(
106 request.instance,
107 &request.source_overrides,
108 request.dirty,
109 )?;
110 }
111
112 let mut source_overrides = request.source_overrides.clone();
115 let mut dirty = request.dirty;
116 match self.store.instance(request.instance)? {
117 Some(existing) if existing.substrate.as_str() != self.substrate.name() => {
118 return Err(EngineError::SubstrateMismatch {
119 instance: request.instance.to_owned(),
120 existing: existing.substrate.as_str().to_owned(),
121 requested: self.substrate.name().to_owned(),
122 });
123 }
124 Some(existing) => {
125 if !request.source_overrides.is_empty() {
126 self.store
127 .update_source_overrides(request.instance, &request.source_overrides)?;
128 } else if existing.status == InstanceStatus::Active {
129 source_overrides = existing.source_overrides.clone();
132 }
133 if request.dirty {
134 self.store.update_dirty(request.instance, true)?;
135 dirty = true;
136 } else if existing.status == InstanceStatus::Active {
137 dirty = existing.dirty;
138 }
139 if existing.status == InstanceStatus::Tombstoned {
141 self.store.revive_instance(
142 request.instance,
143 request.definition_text,
144 &request.source_overrides,
145 request.dirty,
146 )?;
147 dirty = request.dirty;
148 }
149 }
150 None => {
151 match self.store.create_instance(
152 request.instance,
153 self.substrate.name(),
154 request.definition_text,
155 &request.source_overrides,
156 &request.definition_dir,
157 request.dirty,
158 ) {
159 Ok(_) => {}
160 Err(crate::state::StateError::InstanceExists {
163 existing_substrate, ..
164 }) if existing_substrate == self.substrate.name() => {}
165 Err(err) => return Err(err.into()),
166 }
167 }
168 }
169
170 let claim = self.store.claim_lock(request.instance, "up")?;
171 let lease = request
172 .lease
173 .unwrap_or_else(|| self.substrate.default_lease());
174 self.store.renew_lease(request.instance, lease)?;
175
176 let mut request = request;
177 let result = self.run_steps(&mut request, &source_overrides, dirty).await;
178 self.store.release_lock(&claim)?;
179 let outcome = result?;
180 self.store
182 .renew_lease_at_recorded_duration(request.instance)?;
183 Ok(outcome)
184 }
185
186 async fn run_steps(
187 &self,
188 request: &mut UpRequest<'_>,
189 source_overrides: &std::collections::BTreeMap<String, String>,
190 dirty: bool,
191 ) -> Result<UpOutcome, EngineError> {
192 let up_started = Instant::now();
193 let steps = request.def.plan()?;
194 let total = steps.len();
195 let mut null = NullProgress;
196 let progress = request.progress.as_deref_mut().unwrap_or(&mut null);
197 let mut outcome = UpOutcome::default();
198 for (offset, step) in steps.iter().enumerate() {
199 let index = offset + 1;
200 let step_started = Instant::now();
201 let progress_event =
202 |event: StepProgressEvent, code: Option<&'static str>| -> StepProgress {
203 let duration_ms = match event {
204 StepProgressEvent::Started => None,
205 _ => Some(step_started.elapsed().as_millis() as u64),
206 };
207 StepProgress {
208 event,
209 instance: request.instance.to_owned(),
210 step_id: step.id.clone(),
211 step_kind: step.kind,
212 node: step.node.clone(),
213 index,
214 total,
215 code,
216 at_epoch_ms: epoch_ms(),
217 duration_ms,
218 }
219 };
220 progress.on_step(progress_event(StepProgressEvent::Started, None));
221 if let Some(checkpoint) = self.store.checkpoint(request.instance, &step.id)? {
225 let observation = self
226 .substrate
227 .observe(request.instance, &checkpoint)
228 .await
229 .map_err(|fault| {
230 progress
231 .on_step(progress_event(StepProgressEvent::Failed, Some(fault.code)));
232 EngineError::Step {
233 instance: request.instance.to_owned(),
234 step: step.id.clone(),
235 fault,
236 }
237 })?;
238 if observation == Observation::Present {
239 let elapsed = step_started.elapsed().as_millis() as u64;
240 progress.on_step(progress_event(StepProgressEvent::Skipped, None));
241 outcome.skipped.push(step.id.clone());
242 outcome.steps.push(StepTiming {
243 id: step.id.clone(),
244 duration_ms: elapsed,
245 });
246 continue;
247 }
248 }
249 let prior = self.store.checkpoints(request.instance)?;
250 let resource = self
251 .substrate
252 .execute(StepContext {
253 instance: request.instance,
254 def: request.def,
255 step,
256 source_overrides,
257 dirty,
258 prior: &prior,
259 })
260 .await
261 .map_err(|fault| {
262 progress.on_step(progress_event(StepProgressEvent::Failed, Some(fault.code)));
263 EngineError::Step {
264 instance: request.instance.to_owned(),
265 step: step.id.clone(),
266 fault,
267 }
268 })?;
269 self.store.record_checkpoint(
271 request.instance,
272 &step.id,
273 &resource.resource_kind,
274 &resource.resource_id,
275 &resource.payload,
276 )?;
277 let elapsed = step_started.elapsed().as_millis() as u64;
278 progress.on_step(progress_event(StepProgressEvent::Completed, None));
279 outcome.executed.push(step.id.clone());
280 outcome.steps.push(StepTiming {
281 id: step.id.clone(),
282 duration_ms: elapsed,
283 });
284 }
285 outcome.duration_ms = up_started.elapsed().as_millis() as u64;
286 Ok(outcome)
287 }
288
289 pub async fn down(&self, instance: &str) -> Result<DownOutcome, EngineError> {
293 let record = self.store.instance(instance)?.ok_or_else(|| {
294 crate::state::StateError::InstanceNotFound {
295 name: instance.to_owned(),
296 }
297 })?;
298 if record.status == InstanceStatus::Tombstoned {
299 return Ok(DownOutcome::AlreadyDown);
300 }
301 if record.substrate.as_str() != self.substrate.name() {
302 return Err(EngineError::SubstrateMismatch {
303 instance: instance.to_owned(),
304 existing: record.substrate.as_str().to_owned(),
305 requested: self.substrate.name().to_owned(),
306 });
307 }
308
309 let claim = self.store.claim_lock(instance, "down")?;
310 let result = self.destroy_all(instance).await;
311 self.store.release_lock(&claim)?;
312 let survivors = result?;
313 if !survivors.is_empty() {
314 return Err(EngineError::TeardownSurvivors {
315 instance: instance.to_owned(),
316 survivors,
317 });
318 }
319 if let Err(fault) = self.substrate.finalize_teardown(instance).await {
320 return Err(EngineError::Step {
321 instance: instance.to_owned(),
322 step: "finalize_teardown".into(),
323 fault,
324 });
325 }
326 self.store.tombstone_instance(instance)?;
327 self.store.delete_lease(instance)?;
328 self.store.clear_reap_failure(instance)?;
331 Ok(DownOutcome::Destroyed)
332 }
333
334 async fn destroy_all(&self, instance: &str) -> Result<Vec<String>, EngineError> {
335 let mut checkpoints = self.store.checkpoints(instance)?;
336 checkpoints.reverse();
337 let mut survivors = Vec::new();
338 for checkpoint in &checkpoints {
339 if checkpoint.resource_kind == crate::substrate::ACTION_RESOURCE_KIND {
341 self.store
342 .remove_checkpoint(instance, &checkpoint.step_id)?;
343 continue;
344 }
345 if self.substrate.destroy(instance, checkpoint).await.is_err() {
346 survivors.push(checkpoint.resource_id.clone());
347 continue;
348 }
349 match self.substrate.observe(instance, checkpoint).await {
352 Ok(Observation::Gone) => {
353 self.store
354 .remove_checkpoint(instance, &checkpoint.step_id)?;
355 }
356 _ => survivors.push(checkpoint.resource_id.clone()),
357 }
358 }
359 Ok(survivors)
360 }
361
362 fn check_source_override_collisions(
363 &self,
364 instance: &str,
365 source_overrides: &BTreeMap<String, String>,
366 request_dirty: bool,
367 ) -> Result<(), EngineError> {
368 if request_dirty {
369 return Ok(());
370 }
371 let canonical_new: BTreeMap<String, PathBuf> = source_overrides
372 .iter()
373 .filter_map(|(service, path)| {
374 std::fs::canonicalize(path)
375 .ok()
376 .map(|canonical| (service.clone(), canonical))
377 })
378 .collect();
379 for record in self.store.instances()? {
380 if record.status != InstanceStatus::Active || record.name.as_str() == instance {
381 continue;
382 }
383 if record.dirty {
384 continue;
385 }
386 for (service, path) in &record.source_overrides {
387 let Some(want) = canonical_new.get(service) else {
388 continue;
389 };
390 let Ok(have) = std::fs::canonicalize(path) else {
391 continue;
392 };
393 if have == *want {
394 return Err(EngineError::SourceOverrideShared {
395 instance: instance.to_owned(),
396 service: service.clone(),
397 path: path.clone(),
398 other: record.name.as_str().to_owned(),
399 });
400 }
401 }
402 }
403 Ok(())
404 }
405}