Skip to main content

isoloom_render/
configure.rs

1//! The configuration renderers whose output an infrastructure file embeds or builds from (design
2//! `plan/phase4/design.md` §2, §3, §6.1): each renders a machine's resolved recipe
3//! ([`Recipe`], on the plan) and nothing else.
4//!
5//! - [`shell`]: the POSIX library and a recipe's calls, for a Linux VM's set-up and every
6//!   generated Dockerfile (cloud-init carries the VM's script).
7//! - [`cloud_init`]: the same shell script, delivered in a cloud-config `write_files` entry
8//!   (gzip then base64) and run from `runcmd`, for the targets that hand a VM its user data.
9//! - [`powershell`]: the PowerShell library and a recipe's calls, for a Windows VM's set-up.
10//! - [`dockerfile`]: the image a container builds from its recipe, its process Isoloom's
11//!   [`runner`] when it runs services.
12//! - [`files`]: the files they add to a target's: the Dockerfiles, the libraries, the runners and
13//!   the templates filled in for a container's build.
14//!
15//! Every step is rendered in recipe order, behind its guard, so a second apply changes nothing.
16
17use isoloom_plan::Environment;
18use isoloom_plan::configure::{Context, Recipe, Resource, fill};
19use isoloom_spec::syntax::ProjectPath;
20
21use crate::GeneratedFile;
22
23pub mod cloud_init;
24pub mod dockerfile;
25mod password;
26pub mod powershell;
27pub mod runner;
28pub mod shell;
29mod units;
30
31pub use units::{linux_unit, systemd_word};
32
33/// Where the configuration renderers' own files go, from the project folder.
34pub const DIR: &str = ".isoloom/configure";
35
36/// One machine's recipe as its renderers read it: the environment the machine is planned in
37/// (its addresses on the target, which its templates name) and its name.
38#[derive(Debug, Clone, Copy)]
39pub struct Configured<'a> {
40    env: &'a Environment,
41    machine: &'a str,
42    recipe: &'a Recipe,
43}
44
45impl<'a> Configured<'a> {
46    /// Machine `machine` of `env`, when it has a recipe.
47    #[must_use]
48    pub fn of(env: &'a Environment, machine: &'a str) -> Option<Configured<'a>> {
49        let recipe = env.machines.get(machine)?.recipe.as_ref()?;
50        Some(Configured { env, machine, recipe })
51    }
52
53    /// The machine's name.
54    #[must_use]
55    pub fn machine(&self) -> &'a str {
56        self.machine
57    }
58
59    /// Its recipe.
60    #[must_use]
61    pub fn recipe(&self) -> &'a Recipe {
62        self.recipe
63    }
64
65    /// Template `from` filled in for the machine on its target.
66    ///
67    /// # Errors
68    ///
69    /// Why it can't be: its text wasn't read when the files were generated, or a placeholder
70    /// names nothing the lab has (`isoloom validate` reports that first), as the line a recipe
71    /// fails with.
72    pub fn template(&self, from: &ProjectPath) -> Result<String, String> {
73        let text = self
74            .recipe
75            .files
76            .get(from)
77            .ok_or_else(|| format!("the template {from} wasn't read when the files were generated: run `isoloom generate` in the project folder"))?;
78        fill(text, &Context::new(self.env, self.machine)).map_err(|e| format!("the template {from}: {e}"))
79    }
80
81    /// User `user`'s literal `password`, hashed as Linux keeps it (the same for the same lab).
82    #[must_use]
83    pub fn password(&self, user: &str, password: &str) -> String {
84        password::hash(&self.env.name, self.machine, user, password)
85    }
86}
87
88/// A library as a VM's set-up step embeds it: without its blank lines and comment lines (its
89/// directives to shellcheck kept), so the step stays small enough for a VM's user data. The
90/// generated library file keeps them, for people to read.
91#[must_use]
92pub(crate) fn compact(library: &str) -> String {
93    let mut out = String::with_capacity(library.len());
94    for line in library.lines() {
95        let t = line.trim_start();
96        if !t.is_empty() && (!t.starts_with('#') || t.starts_with("# shellcheck")) {
97            out.push_str(line);
98            out.push('\n');
99        }
100    }
101    out
102}
103
104/// Where template `from` of machine `machine`, filled in, is generated for its image's build.
105#[must_use]
106pub fn rendered_path(machine: &str, from: &ProjectPath) -> String {
107    format!("{DIR}/{machine}/rendered/{from}")
108}
109
110/// The files the configuration renderers add to `env`'s: each container's Dockerfile, with its
111/// runner and its filled templates, and the libraries, once, when a recipe uses them (a VM's
112/// set-up embeds its library; the file is the same text, which the validators check).
113#[must_use]
114pub fn files(env: &Environment) -> Vec<GeneratedFile> {
115    let mut out = dockerfile::files(env);
116    let configured = || env.machines.keys().filter_map(|m| Configured::of(env, m));
117    let windows = |c: &Configured<'_>| c.recipe.family == isoloom_plan::configure::Family::Windows;
118    if configured().any(|c| !windows(&c) && uses_library(c.recipe)) && !out.iter().any(|f| f.path == shell::LIBRARY_PATH) {
119        out.push(shell::library_file());
120    }
121    if configured().any(|c| windows(&c) && uses_library(c.recipe)) {
122        out.push(powershell::library_file());
123    }
124    out
125}
126
127/// Whether a recipe has a step its library runs (not only scripts, reboots and playbooks).
128#[must_use]
129pub fn uses_library(recipe: &Recipe) -> bool {
130    recipe
131        .resources()
132        .any(|r| !matches!(r, Resource::Run(_) | Resource::Reboot | Resource::Playbook(_)))
133}
134
135#[cfg(test)]
136mod tests;