use std::fmt;
use isoloom_spec::model::{Machine, Spec};
use crate::environment::MachineShape;
use crate::lifecycle::Step;
use crate::requirements::Requirement;
#[must_use]
pub fn step(m: &Machine) -> Option<Step> {
(!m.files.is_empty()).then(|| Step::Files { files: m.files.clone() })
}
#[must_use]
pub fn reads_project(lifecycle: &[Step]) -> bool {
lifecycle.iter().any(|s| match s {
Step::Run { .. } | Step::Setup { .. } => true,
Step::Configure { resources, .. } => resources.iter().any(|r| matches!(r, crate::configure::Resource::Copy(_))),
Step::Files { files } => files.iter().any(|f| f.source.is_some()),
_ => false,
})
}
pub fn requirements(spec: &Spec, shape: impl Fn(&str) -> Option<MachineShape>) -> Vec<Requirement> {
let mut out = Vec::new();
let owned = spec
.machines
.iter()
.filter(|(n, _)| shape(n) == Some(MachineShape::Container))
.find_map(|(n, m)| m.files.iter().find(|f| f.source.is_some() && f.sets_owner_or_mode()).map(|f| (n, f)));
if let Some((machine, f)) = owned {
out.push(Requirement::OwnedProjectFile {
machine: machine.clone(),
path: f.path.clone(),
});
}
let existing = spec
.machines
.iter()
.find(|(n, m)| shape(n) == Some(MachineShape::External) && !m.files.is_empty());
if let Some((machine, _)) = existing {
out.push(Requirement::ExistingHostFiles { machine: machine.clone() });
}
out
}
pub(crate) fn no_owned_project_file(f: &mut fmt::Formatter<'_>, machine: &str, path: &str) -> fmt::Result {
write!(
f,
"machine `{machine}`'s file `{path}` comes from the project with a `uid`, `gid` or `mode`, and this target places a container's project file as it is: write it as `content` instead"
)
}
pub(crate) fn no_existing_host_files(f: &mut fmt::Formatter<'_>, machine: &str) -> fmt::Result {
write!(
f,
"machine `{machine}` has `files`: Isoloom doesn't write on an existing host, so place them with a `provision:` playbook"
)
}
#[cfg(test)]
mod tests {
use isoloom_spec::model::Target;
use super::reads_project;
use crate::environment::Environment;
use crate::lifecycle::Step;
use crate::requirements::Requirement;
use crate::testing;
const SPEC: &str = "
version: 1
name: files
networks:
lan: { cidr: 10.20.0.0/24 }
machines:
a:
networks: { lan: 10 }
os: debian@12
app: { image: nginx, steps: [{ run: setup.sh }] }
files:
- { path: /etc/a.ini, content: x }
- { path: /etc/b.ini, source: conf/b.ini, mode: '0600' }
overrides:
external: { machines: { a: { ssh: { address: 192.0.2.10 } } } }
";
fn env(target: Target) -> Environment {
testing::planned(&testing::valid(SPEC), target)
}
#[test]
fn a_vm_reads_the_project_for_its_scripts_and_its_files_from_it_only() {
let steps = |yaml: &str| {
let env = testing::environment(&format!("version: 1\nname: t\nmachines:\n a: {{ os: debian@12, {yaml} }}\n"), Target::Vagrant);
env.machines.get("a").map(|m| m.lifecycle.clone()).unwrap_or_default()
};
assert!(!reads_project(&steps("files: [{ path: /a, content: x }]")));
assert!(reads_project(&steps("files: [{ path: /a, source: conf/a }]")));
assert!(reads_project(&steps("app: { steps: [{ run: a.sh }] }")));
assert!(reads_project(&steps("setup: [s.sh]")));
assert!(!reads_project(&steps("command: [sleep, '9']")));
}
#[test]
fn only_a_project_file_with_an_owner_or_a_mode_asks_for_owned_project_files() {
let owned = |files: &str| {
let env = testing::environment(
&format!("version: 1\nname: t\nmachines:\n a: {{ app: {{ image: nginx }}, files: [{files}] }}\n"),
Target::Docker,
);
env.requirements.iter().any(|r| matches!(r, Requirement::OwnedProjectFile { .. }))
};
assert!(owned("{ path: /a, source: conf/a, mode: '0600' }"));
assert!(!owned("{ path: /a, content: x, mode: '0600' }"), "content is written with its mode");
assert!(!owned("{ path: /a, source: conf/a }"));
}
#[test]
fn a_vm_places_its_files_after_the_project_and_before_its_scripts() {
let steps = &env(Target::Vagrant).machines["a"].lifecycle;
let at = |pick: fn(&Step) -> bool| steps.iter().position(pick).expect("planned");
let files = at(|s| matches!(s, Step::Files { files } if files.len() == 2));
assert!(at(|s| matches!(s, Step::Project)) < files && files < at(|s| matches!(s, Step::Run { .. })));
}
#[test]
fn an_owned_project_file_asks_a_container_backend_and_an_existing_host_refuses_files() {
let owned = Requirement::OwnedProjectFile {
machine: "a".into(),
path: "/etc/b.ini".into(),
};
assert!(env(Target::Docker).requirements.contains(&owned));
assert!(!env(Target::Vagrant).requirements.contains(&owned));
let existing = env(Target::External).requirements;
assert!(existing.contains(&Requirement::ExistingHostFiles { machine: "a".into() }), "{existing:?}");
assert_eq!(env(Target::Docker).machines["a"].files.len(), 2);
}
}