isoloom-render 1.0.0

What every Isoloom backend renders with: the backend port, the builders (HCL, Ruby, PowerShell, Dockerfile, the Ansible inventory), a VM's set-up steps, the network pieces and the containers' shared parts.
Documentation
//! The Dockerfile: each step kind's instructions, from the OS's image, and what it builds with.

use isoloom_spec::model::Target;

use super::*;
use crate::testing;

/// The generated files of a machine `m` (YAML) on Docker, with the project's template texts.
fn built(m: &str, templates: &[(&str, &str)]) -> Vec<GeneratedFile> {
    files(&testing::machine(m, Target::Docker, templates))
}

/// The Dockerfile of a machine on `os` with `steps`.
fn dockerfile(os: &str, steps: &str) -> String {
    let found = built(&format!("os: {os}, app: {{ steps: {steps} }}"), &[]);
    found.into_iter().next().expect("a Dockerfile").contents
}

#[test]
fn steps_become_instructions_from_the_os_image() {
    let d = dockerfile(
        "debian@12",
        "[{ packages: [nginx] }, { copy: { from: site/, to: /var/www/, owner: www-data, mode: \"0755\" } }, { run: provision/web.sh }]",
    );
    assert!(d.starts_with("# Generated by isoloom"));
    assert!(
        d.contains("# The container form of machine `m`: its OS's image and its steps.\nFROM debian:12@sha256:"),
        "{d}"
    );
    assert!(
        d.contains("\nWORKDIR /opt/isoloom\nCOPY .isoloom/configure/lib/configure.sh /usr/local/lib/isoloom/configure.sh\n"),
        "{d}"
    );
    assert!(d.contains("RUN . /usr/local/lib/isoloom/configure.sh && isoloom_packages 'nginx'\n"), "{d}");
    assert!(d.contains("COPY --chown=www-data --chmod=0755 site/ /var/www/\n"));
    assert!(d.contains("COPY provision/web.sh provision/web.sh\nRUN sh provision/web.sh\n"));
    assert!(!d.contains("ENTRYPOINT"), "nothing to run: the target keeps it idle");
}

#[test]
fn scripts_alone_need_no_library() {
    let d = dockerfile("debian@12", "[{ run: a.sh, creates: /done }]");
    assert!(d.contains("RUN [ -e /done ] || sh a.sh") && !d.contains("configure.sh"), "{d}");
}

#[test]
fn every_kind_with_a_container_form_is_a_library_call_or_a_copy() {
    let key = format!("key: {{ url: \"https://a.test/k\", sha256: {} }}", "0".repeat(64));
    let d = dockerfile(
        "rocky@9",
        &format!(
            "[{{ repository: {{ name: vendor, url: \"https://a.test/rpm\", {key} }} }}, {{ packages: [{{ name: curl, version: 7.76.1-31.el9 }}] }}, {{ group: {{ name: ops }} }}, {{ user: {{ name: alice, group: ops }} }}, {{ file: {{ path: /srv, state: directory }} }}, {{ command: [touch, /done], creates: /done }}, {{ firewall: {{ allow: [8443] }} }}]"
        ),
    );
    let runs: Vec<&str> = d.lines().filter(|l| l.starts_with("RUN ")).collect();
    assert_eq!(runs.len(), 6, "{d}");
    for (line, function) in runs.iter().zip([
        "isoloom_repository",
        "isoloom_packages",
        "isoloom_group",
        "isoloom_user",
        "isoloom_file",
        "isoloom_command",
    ]) {
        assert!(line.starts_with(&format!("RUN . /usr/local/lib/isoloom/configure.sh && {function} ")), "{line}");
    }
    assert!(!d.contains("firewall"), "a container has no host firewall: {d}");
}

#[test]
fn a_template_is_copied_filled_in_with_its_owner_and_mode() {
    let found = built(
        "os: debian@12, app: { steps: [{ template: { from: conf/app.toml, to: /etc/app.toml, owner: app, group: ops } }] }",
        &[("conf/app.toml", "host = '{{ machine.name }}'\n")],
    );
    assert!(
        found[0]
            .contents
            .contains("COPY --chown=app:ops --chmod=0644 .isoloom/configure/m/rendered/conf/app.toml /etc/app.toml\n"),
        "{}",
        found[0].contents
    );
    let rendered = found
        .iter()
        .find(|f| f.path == ".isoloom/configure/m/rendered/conf/app.toml")
        .expect("the filled template");
    assert_eq!(rendered.contents, "host = 'm'\n");
    let unread = built("os: debian@12, app: { steps: [{ template: { from: conf/app.toml, to: /etc/app.toml } }] }", &[]);
    assert!(
        unread[0].contents.contains("&& isoloom_fail 'the template conf/app.toml wasn'\\''t read"),
        "{}",
        unread[0].contents
    );
    assert!(!unread.iter().any(|f| f.path.contains("/rendered/")), "nothing filled in to copy");
}

#[test]
fn a_container_that_runs_services_builds_its_runner_in_as_its_entrypoint() {
    let found = built(
        "os: debian@12, ports: { web: 80 }, app: { steps: [{ packages: [nginx] }, { service: nginx }] }",
        &[],
    );
    let paths: Vec<&str> = found.iter().map(|f| f.path.as_str()).collect();
    assert_eq!(
        paths,
        [
            ".isoloom/docker/build/m/Dockerfile",
            ".isoloom/configure/lib/configure.sh",
            ".isoloom/configure/m/run"
        ]
    );
    assert!(
        found[0]
            .contents
            .ends_with("COPY --chmod=0755 .isoloom/configure/m/run /usr/local/bin/isoloom-run\nENTRYPOINT [\"/usr/local/bin/isoloom-run\"]\nCMD []\n"),
        "{}",
        found[0].contents
    );
    assert!(!found[0].contents.contains("isoloom_service"), "the runner starts it, not the build");
}

#[test]
fn each_container_built_from_steps_gets_its_dockerfile_and_a_vm_none() {
    let yaml = "version: 1\nname: d\nnetworks: { lan: { cidr: 10.9.0.0/24 } }\nmachines:\n  web: { networks: { lan: 10 }, os: debian@12, app: { steps: [{ packages: [curl] }] } }\n  img: { networks: { lan: 11 }, app: { image: nginx } }\n";
    let found = files(&testing::env(yaml, Target::Docker));
    assert_eq!(found[0].path, ".isoloom/docker/build/web/Dockerfile");
    assert!(found[0].contents.contains("FROM debian:12@sha256:"));
    assert_eq!(files(&testing::env(yaml, Target::Vagrant)), [], "a VM sets itself up");
}

#[test]
fn a_bare_official_image_keeps_its_own_working_folder() {
    // A catalogue entry that is only an official image, with no install or configure steps and
    // the image's own entrypoint, must not have its working folder overridden: an app may rely on
    // it to find its files (OWASP Juice Shop reads `encryptionkeys/` from its image's WORKDIR).
    let d = built("os: debian@12, service: juice-shop", &[])
        .into_iter()
        .next()
        .expect("a Dockerfile")
        .contents;
    assert!(d.contains("FROM docker.io/bkimminich/juice-shop"), "{d}");
    assert!(!d.contains("WORKDIR"), "a bare image keeps its own working folder: {d}");
    assert!(!d.contains("ENTRYPOINT"), "the image's own entrypoint runs: {d}");
}

#[test]
fn each_family_installs_with_its_library_call() {
    for os in ["alpine@3.19", "rocky@9", "centos@7", "ubuntu@24.04"] {
        assert!(dockerfile(os, "[{ packages: [curl] }]").contains("&& isoloom_packages 'curl'\n"), "{os}");
    }
}