Skip to main content

isoloom_spec/syntax/
step.rs

1//! The configuration resource model: an app's `steps`, each one state it ensures behind a guard,
2//! kept in list order by every renderer. The approved five (`run`, `copy`, `packages`, `reboot`,
3//! `playbook`) and eight more: `repository`, `file`, `template`, `user`, `group`, `service`,
4//! `command` (with its required guard) and `firewall` (allow rules only).
5//!
6//! A step is `reboot`, or a map with exactly one kind key; fields such as `creates` and `unless`
7//! sit beside that key. Every step parses into types that hold no invalid value (newtypes for
8//! names, paths, URLs and checksums), and a mistake the design names (a command without its
9//! guard, a download without its checksum, a secret password) is reported with its code, field
10//! and fix.
11
12/// The JSON Schema of `$ty` is that of `$written`, the shape it is written in.
13macro_rules! schema_as {
14    ($ty:ident, $written:ident) => {
15        impl schemars::JsonSchema for $ty {
16            fn schema_name() -> std::borrow::Cow<'static, str> {
17                stringify!($ty).into()
18            }
19
20            fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
21                <$written as schemars::JsonSchema>::json_schema(g)
22            }
23        }
24    };
25}
26
27mod accounts;
28mod files;
29mod input;
30mod services;
31mod software;
32mod words;
33
34pub use accounts::{GroupStep, Password, UserStep};
35pub use files::{Copy, FileState, FileStep, Template};
36pub use services::{Argv, CommandStep, Firewall, FirewallPort, Guard, Invocation, Service};
37pub use software::{Package, Repository, RepositoryKey};
38pub use words::{AbsPath, AptWord, GroupName, HttpsUrl, PackageName, PackageVersion, ProjectPath, RepositoryName, Sha256, UnitName, UserName};
39
40use serde::Deserialize;
41
42/// One step of the portable recipe. Steps install and configure: they run as root from the
43/// project copy at `/opt/isoloom`, with no network to the other machines and no inputs or
44/// secrets (on containers they run at image build time). What needs those goes in `setup`.
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
46#[serde(try_from = "input::StepSchema")]
47#[non_exhaustive]
48pub enum Step {
49    /// A project script (`.sh`, or `.ps1` on Windows), skipped when `creates` exists.
50    Run {
51        /// The script.
52        script: ProjectPath,
53        /// A path whose existence means the step already ran.
54        creates: Option<AbsPath>,
55    },
56    /// A project file or folder copied into the machine.
57    Copy(Copy),
58    /// Packages installed with the OS's own installer (Windows features on Windows), never
59    /// empty.
60    Packages(Vec<Package>),
61    /// The machine restarts, and the next step runs once it is back (no container form).
62    Reboot,
63    /// An Ansible playbook of the project, run in the machine (no container form in 1.0).
64    Playbook(ProjectPath),
65    /// A vendor's package repository, its key pinned.
66    Repository(Repository),
67    /// A folder, a removal, a link or a pinned download.
68    File(FileStep),
69    /// A project file with its placeholders filled in.
70    Template(Template),
71    /// A local user.
72    User(UserStep),
73    /// A local group.
74    Group(GroupStep),
75    /// A daemon: enabled and started on VMs, the container's process in containers.
76    Service(Service),
77    /// A command run once, behind its guard.
78    Command(CommandStep),
79    /// Ports opened in the OS's own firewall.
80    Firewall(Firewall),
81}
82
83impl Step {
84    /// The step's kind, as its key is written (`run`, `copy`, `reboot`).
85    #[must_use]
86    pub fn kind(&self) -> &'static str {
87        match self {
88            Step::Run { .. } => "run",
89            Step::Copy(_) => "copy",
90            Step::Packages(_) => "packages",
91            Step::Reboot => "reboot",
92            Step::Playbook(_) => "playbook",
93            Step::Repository(_) => "repository",
94            Step::File(_) => "file",
95            Step::Template(_) => "template",
96            Step::User(_) => "user",
97            Step::Group(_) => "group",
98            Step::Service(_) => "service",
99            Step::Command(_) => "command",
100            Step::Firewall(_) => "firewall",
101        }
102    }
103
104    /// Why the step has no container form, when it has none.
105    #[must_use]
106    pub fn container_refusal(&self) -> Option<&'static str> {
107        match self {
108            Step::Reboot => Some("a reboot needs a machine with its own kernel and boot: a container has neither"),
109            Step::Playbook(_) => Some("a playbook step runs in a VM only in 1.0"),
110            Step::Run { script, .. } if has_extension(script.as_str(), "ps1") => Some("a PowerShell script means Windows, which has no container form in 1.0"),
111            _ => None,
112        }
113    }
114}
115
116/// Whether `path` ends in `.<extension>`, in any case.
117pub(crate) fn has_extension(path: &str, extension: &str) -> bool {
118    std::path::Path::new(path).extension().is_some_and(|e| e.eq_ignore_ascii_case(extension))
119}
120
121impl schemars::JsonSchema for Step {
122    fn schema_name() -> std::borrow::Cow<'static, str> {
123        "Step".into()
124    }
125
126    fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
127        <input::StepSchema as schemars::JsonSchema>::json_schema(g)
128    }
129}
130
131#[cfg(test)]
132mod tests;