Skip to main content

ambient_ci/
plan.rs

1//! Different kinds of CI plans.
2
3#![allow(clippy::result_large_err)]
4
5use std::{
6    collections::HashMap,
7    fmt::Debug,
8    path::{Path, PathBuf},
9};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    action::{Context, PostPlanAction, PrePlanAction, RunnableAction, UnsafeAction},
15    config::Config,
16    project::{Project, State},
17    qemu,
18    runlog::RunLogSource,
19};
20
21/// A complete plan, as used by users. This can only contain
22/// unsafe actions that are to be executed in a virtual machine.
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct Plan {
25    steps: Vec<UnsafeAction>,
26}
27
28impl Plan {
29    /// Load plan from a file.
30    pub fn from_file(filename: &Path) -> Result<Self, PlanError> {
31        let plan = std::fs::read(filename).map_err(|e| PlanError::PlanOpen(filename.into(), e))?;
32        let plan = serde_norway::from_slice(&plan)
33            .map_err(|e| PlanError::PlanParse(filename.into(), e))?;
34        Ok(plan)
35    }
36
37    /// Write plan to a file.
38    pub fn to_file(&self, filename: &Path) -> Result<(), PlanError> {
39        let plan = serde_norway::to_string(&self).map_err(PlanError::PlanSerialize)?;
40        std::fs::write(filename, plan).map_err(|e| PlanError::PlanWrite(filename.into(), e))?;
41        Ok(())
42    }
43
44    /// Append another action to the plan.
45    pub fn push(&mut self, action: UnsafeAction) {
46        self.steps.push(action);
47    }
48
49    /// Iterator over actions in a plan.
50    pub fn iter(&self) -> impl Iterator<Item = &UnsafeAction> {
51        self.steps.iter()
52    }
53}
54
55/// A runnable plan. This contains actual actions that can be executed.
56/// It does not guard against safe vs unsafe actions, anything goes.
57#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
58#[serde(deny_unknown_fields)]
59pub struct RunnablePlan {
60    steps: Vec<RunnableAction>,
61    executor_drive: Option<String>,
62    source_drive: Option<String>,
63    artifact_drive: Option<String>,
64    cache_drive: Option<String>,
65    deps_drive: Option<String>,
66    workspace_dir: Option<String>,
67    source_dir: Option<String>,
68    deps_dir: Option<String>,
69    cache_dir: Option<String>,
70    artifacts_dir: Option<String>,
71
72    #[serde(default)]
73    envs: HashMap<String, Vec<u8>>,
74}
75
76impl RunnablePlan {
77    #[cfg(test)]
78    fn parse_str(yaml: &str) -> Result<Self, PlanError> {
79        serde_norway::from_str(yaml).map_err(PlanError::PlanParseStr)
80    }
81
82    /// Load plan from a file.
83    pub fn from_file(filename: &Path) -> Result<Self, PlanError> {
84        let plan = std::fs::read(filename).map_err(|e| PlanError::PlanOpen(filename.into(), e))?;
85        let plan = String::from_utf8_lossy(&plan);
86        let plan: Self =
87            serde_norway::from_str(&plan).map_err(|e| PlanError::PlanParse(filename.into(), e))?;
88
89        for step in plan.steps.iter() {
90            if let RunnableAction::HttpGet(x) = step {
91                for item in x.items() {
92                    if item
93                        .filename()
94                        .as_os_str()
95                        .as_encoded_bytes()
96                        .contains(&b'/')
97                    {
98                        return Err(PlanError::FilenameIsNotBasename(filename.to_path_buf()));
99                    }
100                }
101            }
102        }
103
104        Ok(plan)
105    }
106
107    /// Convert plan to a YAML string.
108    pub fn to_string(&self) -> Result<String, PlanError> {
109        serde_norway::to_string(self).map_err(PlanError::PlanSerialize)
110    }
111
112    /// Write plan to a file as YAML.
113    pub fn to_file(&self, filename: &Path) -> Result<(), PlanError> {
114        let plan = serde_norway::to_string(&self).map_err(PlanError::PlanSerialize)?;
115        std::fs::write(filename, plan).map_err(|e| PlanError::PlanWrite(filename.into(), e))?;
116        Ok(())
117    }
118
119    /// Environment variables to set in context.
120    pub fn envs(&self) -> &HashMap<String, Vec<u8>> {
121        &self.envs
122    }
123
124    /// Append an action to the plan.
125    pub fn push(&mut self, action: RunnableAction) {
126        self.steps.push(action);
127    }
128
129    /// Append any number of actions to a plan.
130    pub fn push_unsafe_actions<'a>(&mut self, actions: impl Iterator<Item = &'a UnsafeAction>) {
131        for action in actions {
132            self.push(RunnableAction::from_unsafe_action(action));
133        }
134    }
135
136    /// Set an environment variables from pre-plan.
137    pub fn carry_over_from_context(&mut self, context: &Context) {
138        for (k, v) in context.plan_envs().iter() {
139            self.envs.insert(k.clone(), v.to_vec());
140        }
141    }
142
143    /// Iterator over actions in a plan.
144    pub fn iter(&self) -> impl Iterator<Item = &RunnableAction> {
145        self.steps.iter()
146    }
147
148    /// Execute actions in the plan.
149    pub fn execute(&self, source: RunLogSource, context: &mut Context) -> Result<(), PlanError> {
150        context
151            .set_envs_from_plan(self)
152            .map_err(PlanError::Context)?;
153        for action in self.steps.iter() {
154            context.runlog().execute_action(source, action);
155            let result = action.execute(context);
156            match &result {
157                Ok(()) => context.runlog().action_succeeded(source, action),
158                Err(_) => {
159                    context.runlog().action_failed(source, action);
160                    // FIXME: this needs to be passed into the runlog, but that will
161                    // FIXME: error to be serde::Deserialize
162                    // let mut source = err.source();
163                    // while let Some(src) = source {
164                    //     error!("caused by: {src}");
165                    //     source = src.source();
166                    // }
167                    result?;
168                }
169            }
170        }
171
172        if !self.steps.is_empty() {
173            context.runlog().plan_succeeded(source);
174        }
175        Ok(())
176    }
177
178    /// Return executor drive, as set in the plan.
179    pub fn executor_drive(&self) -> Option<&String> {
180        self.executor_drive.as_ref()
181    }
182
183    /// Return source drive, as set in the plan.
184    pub fn source_drive(&self) -> Option<&String> {
185        self.source_drive.as_ref()
186    }
187
188    /// Return artifact drive, as set in the plan.
189    pub fn run_artifact_drive(&self) -> Option<&String> {
190        self.artifact_drive.as_ref()
191    }
192
193    /// Return cache drive, as set in the plan.
194    pub fn cache_drive(&self) -> Option<&String> {
195        self.cache_drive.as_ref()
196    }
197
198    /// Return dependencies drive, as set in the plan.
199    pub fn deps_drive(&self) -> Option<&String> {
200        self.deps_drive.as_ref()
201    }
202
203    /// Return root directory of workspace.
204    pub fn workspace_dir(&self) -> Option<&String> {
205        self.workspace_dir.as_ref()
206    }
207
208    /// Return source directory.
209    pub fn source_dir(&self) -> Option<&String> {
210        self.source_dir.as_ref()
211    }
212
213    /// Return dependencies directory.
214    pub fn deps_dir(&self) -> Option<&String> {
215        self.deps_dir.as_ref()
216    }
217
218    /// Return cache directory.
219    pub fn cache_dir(&self) -> Option<&String> {
220        self.cache_dir.as_ref()
221    }
222
223    /// Return artifacts directory.
224    pub fn artifacts_dir(&self) -> Option<&String> {
225        self.artifacts_dir.as_ref()
226    }
227
228    /// Set executor drive.
229    pub fn set_executor_drive(&mut self, path: &str) {
230        self.executor_drive = Some(path.into());
231    }
232
233    /// Set source drive.
234    pub fn set_source_drive(&mut self, path: &str) {
235        self.source_drive = Some(path.into());
236    }
237
238    /// Set artifacts drive.
239    pub fn set_artifact_drive(&mut self, path: &str) {
240        self.artifact_drive = Some(path.into());
241    }
242
243    /// Set cache drive.
244    pub fn set_cache_drive(&mut self, path: &str) {
245        self.cache_drive = Some(path.into());
246    }
247
248    /// Set dependencies drive.
249    pub fn set_deps_drive(&mut self, path: &str) {
250        self.deps_drive = Some(path.into());
251    }
252
253    /// Set root directory of workspace.
254    pub fn set_workspace_dir(&mut self, path: &str) {
255        self.workspace_dir = Some(path.into());
256    }
257
258    /// Set source directory.
259    pub fn set_source_dir(&mut self, path: &str) {
260        self.source_dir = Some(path.into());
261    }
262
263    /// Set depencencies directory.
264    pub fn set_deps_dir(&mut self, path: &str) {
265        self.deps_dir = Some(path.into());
266    }
267
268    /// Set cache directory.
269    pub fn set_cache_dir(&mut self, path: &str) {
270        self.cache_dir = Some(path.into());
271    }
272
273    /// Set artifacts directory.
274    pub fn set_artifacts_dir(&mut self, path: &str) {
275        self.artifacts_dir = Some(path.into());
276    }
277
278    /// Set directories that have not been set yet.
279    pub fn set_unset_dirs(&mut self, path: &str) {
280        fn set(s: &mut Option<String>, path: &str) {
281            if s.is_none() {
282                *s = Some(path.to_string());
283            }
284        }
285
286        set(&mut self.workspace_dir, path);
287        set(&mut self.source_dir, path);
288        set(&mut self.deps_dir, path);
289        set(&mut self.cache_dir, path);
290        set(&mut self.artifacts_dir, path);
291    }
292}
293
294/// Construct runnable plans for pre-plan, plan, and post-plan.
295pub fn construct_all_plans(
296    config: &Config,
297    project_name: &str,
298    project: &Project,
299    state: &State,
300) -> Result<(RunnablePlan, RunnablePlan, RunnablePlan), PlanError> {
301    let pre_plan = runnable_plan_from_pre_plan_actions(project, state, project.pre_plan());
302    let plan = construct_runnable_plan(project.plan())?;
303    let post_plan = runnable_plan_from_post_plan_actions(
304        config,
305        project_name,
306        project,
307        state,
308        project.post_plan(),
309    );
310    Ok((pre_plan, plan, post_plan))
311}
312
313/// Construct a [`RunnablePlan`] from unsafe actions.
314pub fn construct_runnable_plan(actions: &[UnsafeAction]) -> Result<RunnablePlan, PlanError> {
315    let prologue = [
316        UnsafeAction::mkdir(Path::new(qemu::WORKSPACE_DIR)),
317        UnsafeAction::mkdir(Path::new(qemu::ARTIFACTS_DIR)),
318        UnsafeAction::tar_extract(Path::new(qemu::SOURCE_DRIVE), Path::new(qemu::SOURCE_DIR)),
319        UnsafeAction::tar_extract(Path::new(qemu::DEPS_DRIVE), Path::new(qemu::DEPS_DIR)),
320        UnsafeAction::tar_extract(Path::new(qemu::CACHE_DRIVE), Path::new(qemu::CACHE_DIR)),
321        UnsafeAction::shell("ln -sf /ci /workspace"),
322        UnsafeAction::shell("git config --global user.name 'Ambient CI'"),
323        UnsafeAction::shell("git config --global user.email ambient@example.com"),
324    ];
325
326    let epilogue = [
327        UnsafeAction::tar_create(Path::new(qemu::CACHE_DRIVE), Path::new(qemu::CACHE_DIR)),
328        UnsafeAction::tar_create(
329            Path::new(qemu::ARTIFACT_DRIVE),
330            Path::new(qemu::ARTIFACTS_DIR),
331        ),
332    ];
333
334    let mut runnable = RunnablePlan::default();
335    runnable.set_executor_drive(qemu::EXECUTOR_DRIVE);
336    runnable.set_source_drive(qemu::SOURCE_DRIVE);
337    runnable.set_artifact_drive(qemu::ARTIFACT_DRIVE);
338    runnable.set_cache_drive(qemu::CACHE_DRIVE);
339    runnable.set_deps_drive(qemu::DEPS_DRIVE);
340    runnable.set_workspace_dir(qemu::WORKSPACE_DIR);
341    runnable.set_source_dir(qemu::SOURCE_DIR);
342    runnable.set_artifacts_dir(qemu::ARTIFACTS_DIR);
343    runnable.set_deps_dir(qemu::DEPS_DIR);
344    runnable.set_cache_dir(qemu::CACHE_DIR);
345
346    runnable.push_unsafe_actions(prologue.iter());
347    runnable.push_unsafe_actions(actions.iter());
348    runnable.push_unsafe_actions(epilogue.iter());
349
350    Ok(runnable)
351}
352
353/// Construct a [`RunnablePlan`] from a pre-plan actions.
354pub fn runnable_plan_from_pre_plan_actions(
355    project: &Project,
356    state: &State,
357    actions: &[PrePlanAction],
358) -> RunnablePlan {
359    fn path(path: &Path) -> String {
360        path.to_string_lossy().into_owned()
361    }
362
363    let mut plan = RunnablePlan::default();
364    plan.set_cache_dir(&path(&state.cachedir()));
365    plan.set_deps_dir(&path(&state.dependenciesdir()));
366    plan.set_artifacts_dir(&path(&state.artifactsdir()));
367    plan.set_source_dir(&path(project.source()));
368    for action in actions {
369        plan.push(RunnableAction::from_pre_plan_action(action));
370    }
371    plan
372}
373
374/// Construct a [`RunnablePlan`] from post-plan actions.
375pub fn runnable_plan_from_post_plan_actions(
376    config: &Config,
377    project_name: &str,
378    project: &Project,
379    state: &State,
380    actions: &[PostPlanAction],
381) -> RunnablePlan {
382    fn path(path: &Path) -> String {
383        path.to_string_lossy().into_owned()
384    }
385
386    let mut plan = RunnablePlan::default();
387    plan.set_cache_dir(&path(&state.cachedir()));
388    plan.set_deps_dir(&path(&state.dependenciesdir()));
389    plan.set_artifacts_dir(&path(&state.artifactsdir()));
390    plan.set_source_dir(&path(project.source()));
391    for action in actions {
392        plan.push(RunnableAction::from_post_plan_action(
393            action,
394            config.rsync_target_for_project(project_name).as_deref(),
395            config.dput_target(),
396        ));
397    }
398    plan
399}
400
401/// Errors from handling plans.
402#[derive(Debug, thiserror::Error)]
403pub enum PlanError {
404    /// Can't open plan file.
405    #[error("failed to read CI plan file: {0}")]
406    PlanOpen(PathBuf, #[source] std::io::Error),
407
408    /// Can't parse plan as YAML from a file.
409    #[error("failed to parse CI plan file as YAML: {0}")]
410    PlanParse(PathBuf, #[source] serde_norway::Error),
411
412    /// Can't parse CI plan as YAML from string.
413    #[error("failed to parse CI plan")]
414    PlanParseStr(#[source] serde_norway::Error),
415
416    /// Can't serialize a plan as YAML.
417    #[error("failed to serialize CI plan as YAML")]
418    PlanSerialize(#[source] serde_norway::Error),
419
420    /// Can't write plan to file.
421    #[error("failed to write CI plan file: {0}")]
422    PlanWrite(PathBuf, #[source] std::io::Error),
423
424    /// Forwarded from action.
425    #[error(transparent)]
426    Action(#[from] crate::action::ActionError),
427
428    /// The `http-get` filename contains a directory.
429    #[error("the filename in a URL/filename pair contains a directory")]
430    FilenameIsNotBasename(PathBuf),
431
432    /// Can't create a [`Context`].
433    #[error("failed to create a context for executing actions")]
434    Context(#[source] crate::action::ActionError),
435}
436
437#[cfg(test)]
438mod test {
439    use super::*;
440
441    #[test]
442    fn round_trip() -> Result<(), Box<dyn std::error::Error>> {
443        let mut plan = RunnablePlan::default();
444        plan.set_source_dir("/src");
445
446        let s = plan.to_string()?;
447        let des = RunnablePlan::parse_str(&s)?;
448
449        assert_eq!(plan, des);
450        Ok(())
451    }
452}