1mod configs;
6mod hardware;
7mod process;
8mod secrets;
9mod service;
10
11pub(crate) use service::readiness;
12
13use std::fmt;
14
15use indexmap::IndexMap;
16use isoloom_plan::Environment;
17use isoloom_render::backend::Files;
18use isoloom_render::container::PROBE_IMAGE;
19use isoloom_render::{GeneratedFile, OUTPUT_DIR, header, trunks};
20use isoloom_spec::model::{Arch, Target};
21use isoloom_spec::validate::Cidr;
22
23use crate::error::{Error, Result};
24use crate::file::{Attachment, ComposeFile, NetworkMode, Restart, Service, Volume};
25use crate::images::TOOLBOX_IMAGE;
26use crate::layout::DIR;
27use crate::machine::Machine;
28use crate::names::{probe_service, tool_service};
29use crate::{check_runners, ipv6, labels, networks, outside, router, storage};
30
31const USAGE: &str = "# Start: docker compose -f .isoloom/docker/compose.yml up -d --wait\n# Checks: isoloom test docker (or: docker compose -f .isoloom/docker/compose.yml --profile check run --rm isoloom-check, and isoloom-check-<machine>)\n# Stop: docker compose -f .isoloom/docker/compose.yml down -v\n";
33
34pub fn render(target: Target, env: &Environment) -> Result<Files> {
42 let Rendered { file, files: mounted } = compose_file(target, env)?;
43 let yaml = to_yaml(target, &file)?;
44 let mut files = vec![GeneratedFile {
45 path: format!("{OUTPUT_DIR}/{DIR}/compose.yml"),
46 contents: format!("{}{USAGE}{}{}{}\n{yaml}", header("#"), secrets::usage(&file), configs::usage(env), Moved(env)),
47 }];
48 files.extend(mounted);
49 Ok(files)
50}
51
52#[derive(Debug, Clone)]
54pub(crate) struct Rendered {
55 pub(crate) file: ComposeFile,
57 pub(crate) files: Files,
59}
60
61pub(crate) fn compose_file(target: Target, env: &Environment) -> Result<Rendered> {
65 if let Some(refusal) = isoloom_plan::refusal(&env.requirements, &crate::backend::capabilities()) {
66 return Err(Error::Refused { target, refusal });
67 }
68 let mut out = Out::default();
69 let ts = trunks::containers(env.spec());
70 router::services(env, &ts, &mut out.services);
71 for m in Machine::containers(env) {
72 service::machine(env, m, &ts, &mut out);
73 }
74 storage::folders(env, &mut out.services);
75 let runners = check_runners::services(env, &mut out.services);
76 tools(env, &mut out.services);
77 let mut file = ComposeFile {
78 name: env.name.clone(),
79 networks: networks::declared(env, &ts),
80 volumes: volumes(env, &out.probe_archs),
81 secrets: secrets::of_file(env),
82 configs: configs::declared(env),
83 ..ComposeFile::default()
84 };
85 probes(env, &out.probe_archs, &mut out.services);
86 file.services = out.services;
87 labels::mark(env, &mut file);
88 file.networks.extend(outside::networks(env));
90 let mut files = runners;
91 files.extend(out.files);
92 files.extend(isoloom_render::configure::dockerfile::files(env));
93 Ok(Rendered { file, files })
94}
95
96pub(crate) fn to_yaml(target: Target, file: &ComposeFile) -> Result<String> {
98 serde_yaml_ng::to_string(file).map_err(|source| Error::Unwritable { target, source })
99}
100
101#[derive(Default)]
103struct Out {
104 services: IndexMap<String, Service>,
105 files: Vec<GeneratedFile>,
107 probe_archs: Vec<Arch>,
109}
110
111struct Moved<'a>(&'a Environment);
113
114impl fmt::Display for Moved<'_> {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 for (name, n) in self.0.networks.iter().filter(|(_, n)| n.declared_cidr != n.cidr) {
117 writeln!(
118 f,
119 "# On Docker, network `{name}` uses {} instead of {} (same last octets).",
120 n.cidr, n.declared_cidr
121 )?;
122 }
123 Ok(())
124 }
125}
126
127fn tools(env: &Environment, services: &mut IndexMap<String, Service>) {
130 let spec = env.spec();
131 for (i, (name, tool)) in spec.tools.iter().enumerate() {
132 let shell = name == "shell";
133 let mut t = Service {
134 image: Some(env.images.pull(tool.image.as_deref().unwrap_or(TOOLBOX_IMAGE))),
135 hostname: Some(name.clone()),
136 command: tool.command.clone(),
137 restart: Some(Restart::UnlessStopped),
138 ..Service::default()
139 };
140 if tool.command.is_empty() && shell {
141 t.init = true;
142 t.entrypoint = vec!["sleep".into(), "infinity".into()];
143 }
144 if shell {
145 t.cap_add = vec!["NET_ADMIN".into(), "NET_RAW".into()];
146 }
147 for (net, n) in &spec.networks {
149 if ipv6::only(env, net) {
151 t.networks.insert(net.clone(), None);
152 } else if let Some(cidr) = Cidr::parse(&n.cidr) {
153 t.networks.insert(net.clone(), Some(Attachment::at(cidr.tool(i))));
154 }
155 }
156 if let (Some(port), Some(host)) = (tool.port, tool.publish) {
157 t.ports = vec![format!("${{ISOLOOM_PUBLISH_ADDRESS:-127.0.0.1}}:{host}:{port}")];
158 }
159 services.insert(tool_service(name), t);
160 }
161}
162
163fn probes(env: &Environment, archs: &[Arch], services: &mut IndexMap<String, Service>) {
165 for arch in archs {
166 let name = probe_service(*arch);
167 let p = Service {
168 image: Some(env.images.pull(PROBE_IMAGE)),
169 platform: Some(arch.docker_platform().to_owned()),
170 entrypoint: vec!["/bin/cp".into(), "/bin/busybox".into(), "/probe/busybox".into()],
171 volumes: vec![format!("{name}:/probe").into()],
172 network_mode: Some(NetworkMode::None),
173 restart: Some(Restart::No),
174 ..Service::default()
175 };
176 services.insert(name, p);
177 }
178}
179
180fn volumes(env: &Environment, archs: &[Arch]) -> IndexMap<String, Volume> {
182 let mut out = storage::declared(env);
183 out.extend(archs.iter().map(|a| (probe_service(*a), Volume::default())));
184 out
185}
186
187#[cfg(test)]
188mod tests {
189 use isoloom_plan::Environment;
190 use isoloom_spec::model::Target;
191
192 use crate::start::{StartPlan, start_plan};
193
194 #[test]
195 fn the_start_plan_reads_the_same_from_the_text_as_from_the_typed_file() {
196 for example in crate::testing::examples() {
197 let env = Environment::build(&example.spec, Target::Docker, None);
198 let Ok(rendered) = super::compose_file(Target::Docker, &env) else { continue };
199 let yaml = super::to_yaml(Target::Docker, &rendered.file).expect("writes");
200 assert_eq!(StartPlan::of(&rendered.file), start_plan(&yaml).expect("reads back"), "{}", example.name);
201 }
202 }
203}