Skip to main content

isoloom_compose/
backend.rs

1//! The Compose backend: what Compose can render, on the user's Docker host and on a VM that runs
2//! the Compose file.
3
4use isoloom_manifest::Manifest;
5use isoloom_plan::{Capabilities, Environment, Feature};
6use isoloom_render::Error as RenderError;
7use isoloom_render::backend::{Backend, Files};
8use isoloom_render::run_manifest::guest::GuestCompose;
9use isoloom_spec::model::Target;
10
11use crate::render;
12
13/// The Compose backend of the `docker` and `hosted` targets (a hosting service runs the same
14/// Compose file): the target it renders for.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct ComposeBackend(pub Target);
17
18impl Backend for ComposeBackend {
19    fn target(&self) -> Target {
20        self.0
21    }
22
23    fn capabilities(&self) -> Capabilities {
24        capabilities()
25    }
26
27    fn render(&self, env: &Environment) -> Result<Files, RenderError> {
28        Ok(render::render(self.0, env)?)
29    }
30
31    /// The `docker` target's manifest; a hosting service runs the file its own way.
32    fn run_manifest(&self, env: &Environment) -> Result<Option<Manifest>, RenderError> {
33        if self.0 != Target::Docker {
34            return Ok(None);
35        }
36        let file = render::compose_file(self.0, env)?.file;
37        Ok(Some(crate::manifest::docker(env, &file)?))
38    }
39}
40
41/// What runs inside `docker-vm`'s VM for `env` (planned for the `docker` target): the `docker`
42/// target's Compose file, its containers, and the commands that start, check and read them there
43/// (the backend that makes the VM runs them through its own way in).
44///
45/// # Errors
46///
47/// [`crate::Error::Refused`] when `env` needs what Compose can't do; a manifest error for a name
48/// the run manifest refuses (a bug: the spec checks them).
49pub fn guest(env: &Environment) -> crate::Result<GuestCompose> {
50    let file = render::compose_file(Target::Docker, env)?.file;
51    crate::manifest::guest(env, &file)
52}
53
54/// What Compose can render: network appliances (wired the way Docker attaches networks), aliases,
55/// gateway machines, ulimits, `tc` (in a sidecar), IPv6 networks of every kind, secrets
56/// (Compose's own, owners included), project files with their owner and mode (copied in from
57/// Compose's environment), existing Docker networks, and the host's GPUs and devices
58/// (the containers run on the user's own Docker host, which has them). No controller: no
59/// `provision:`, no Ansible checks; `exec` checks run inside a container. No bridged
60/// networks: a container reaches the host's LAN through a macvlan network made beforehand
61/// (`external`). Data disks are named volumes, a tmpfs takes a mode, and a volume can be kept
62/// past `down` or exist already; no raw disk (a container gets no block device).
63pub(crate) fn capabilities() -> Capabilities {
64    Capabilities::of([
65        Feature::NetworkAppliances,
66        Feature::Gateways,
67        Feature::MachineAliases,
68        Feature::Ulimits,
69        Feature::TrafficControl,
70        Feature::Ipv6,
71        Feature::Ipv6Only,
72        Feature::Ipv6Auto,
73        Feature::Secrets,
74        Feature::SecretOwners,
75        Feature::OwnedProjectFiles,
76        Feature::ExternalNetworks,
77        Feature::MachineProcess,
78        Feature::ContainerGpus,
79        Feature::ContainerDevices,
80        // Docker's DNS answers the names under the private zone's domain (network aliases).
81        Feature::PrivateDns,
82        Feature::DataDisks,
83        Feature::TmpfsModes,
84        Feature::PersistentVolumes,
85        Feature::ExternalVolumes,
86    ])
87}
88
89/// What a Compose file run on a VM can render (`docker-vm`, `cloud-docker`): Compose's, but
90/// existing networks, which a fresh Docker host VM has none of, and the host's GPUs and
91/// devices, which a VM Isoloom creates doesn't have, and volumes kept past `down` or existing
92/// already (the storage goes with the VM). Secrets reach the VM over its own
93/// connection (`isoloom run`), and Compose takes them from there.
94#[must_use]
95pub fn capabilities_on_a_vm() -> Capabilities {
96    Capabilities::of([
97        Feature::NetworkAppliances,
98        Feature::Gateways,
99        Feature::MachineAliases,
100        Feature::Ulimits,
101        Feature::TrafficControl,
102        Feature::DataDisks,
103        Feature::TmpfsModes,
104        Feature::Ipv6,
105        Feature::Ipv6Only,
106        Feature::Ipv6Auto,
107        Feature::Secrets,
108        Feature::SecretOwners,
109        Feature::OwnedProjectFiles,
110        Feature::MachineProcess,
111        Feature::PrivateDns,
112    ])
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn a_vm_runs_what_compose_does_but_what_only_the_users_own_host_has() {
121        let own_host_only = [
122            Feature::ExternalNetworks,
123            Feature::ContainerGpus,
124            Feature::ContainerDevices,
125            Feature::PersistentVolumes,
126            Feature::ExternalVolumes,
127        ];
128        for f in own_host_only {
129            assert!(capabilities().has(f) && !capabilities_on_a_vm().has(f), "{f:?}");
130        }
131        assert!(capabilities_on_a_vm().has(Feature::Secrets));
132    }
133}