Skip to main content

arcbox_migration/
model.rs

1//! Normalized migration model types.
2
3use crate::docker_types::NetworkIpamConfig;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8/// Supported migration source runtimes.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "kebab-case")]
11pub enum SourceKind {
12    /// Docker Desktop on macOS.
13    DockerDesktop,
14    /// OrbStack on macOS.
15    OrbStack,
16}
17
18impl SourceKind {
19    /// Returns the stable CLI/protocol identifier for the source kind.
20    #[must_use]
21    pub const fn as_str(self) -> &'static str {
22        match self {
23            Self::DockerDesktop => "docker-desktop",
24            Self::OrbStack => "orbstack",
25        }
26    }
27}
28
29/// Source configuration used by the planner and executor.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct SourceConfig {
32    /// Selected source runtime.
33    pub kind: SourceKind,
34    /// Source Docker Engine socket path.
35    pub socket_path: PathBuf,
36}
37
38/// Minimal source identity discovered during preflight.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct SourceInfo {
41    /// Selected source runtime.
42    pub kind: SourceKind,
43    /// Source Docker Engine socket path.
44    pub socket_path: PathBuf,
45    /// Docker daemon name.
46    pub daemon_name: String,
47    /// Reported server version.
48    pub server_version: String,
49    /// Reported operating system.
50    pub operating_system: String,
51    /// Reported architecture.
52    pub architecture: String,
53}
54
55/// A fully normalized migration plan.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct MigrationPlan {
58    /// Source identity.
59    pub source: SourceInfo,
60    /// Helper image reference used for temporary volume-mount containers.
61    pub helper_image: String,
62    /// Images that will be imported into ArcBox.
63    pub images: Vec<ImagePlan>,
64    /// Volumes that will be imported into ArcBox.
65    pub volumes: Vec<VolumePlan>,
66    /// Networks that will be recreated in ArcBox.
67    pub networks: Vec<NetworkPlan>,
68    /// Containers that will be recreated in ArcBox.
69    pub containers: Vec<ContainerPlan>,
70    /// Resources that are out of scope for v1. Blocking: execution refuses to
71    /// start while any are present.
72    pub unsupported_resources: Vec<String>,
73    /// Advisory problems that do not block execution but will likely surprise
74    /// the user (for example, a bind mount whose source is missing).
75    pub warnings: Vec<String>,
76    /// Replace actions that require confirmation.
77    pub replacements: ReplacementSummary,
78    /// Source volume blockers caused by running containers.
79    pub blockers: Vec<RunningVolumeBlocker>,
80}
81
82/// Source image transfer description.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct ImagePlan {
85    /// Source image identifier.
86    pub image_id: String,
87    /// Every reference passed to `docker save`. Listing all tags is what keeps
88    /// them: `docker save` preserves an image's other tags only when the
89    /// argument omits a tag, so exporting one `repo:tag` drops the rest.
90    pub export_references: Vec<String>,
91    /// Repo tags preserved by the source daemon.
92    pub repo_tags: Vec<String>,
93    /// Repo tags that will overwrite an existing ArcBox tag.
94    pub replace_tags: Vec<String>,
95}
96
97impl ImagePlan {
98    /// Returns the reference used when a container refers to this image.
99    #[must_use]
100    pub fn primary_reference(&self) -> &str {
101        self.export_references
102            .first()
103            .map_or(self.image_id.as_str(), String::as_str)
104    }
105}
106
107/// Source volume transfer description.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct VolumePlan {
110    /// Source volume name.
111    pub name: String,
112    /// Volume driver.
113    pub driver: String,
114    /// Volume labels preserved during recreate.
115    pub labels: HashMap<String, String>,
116    /// Driver options preserved during recreate.
117    pub options: HashMap<String, String>,
118    /// Whether the target volume will be replaced.
119    pub replace_existing: bool,
120    /// Source containers referencing the volume.
121    pub attached_containers: Vec<String>,
122}
123
124/// Source network transfer description.
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct NetworkPlan {
127    /// Source network name.
128    pub name: String,
129    /// Source network identifier.
130    pub id: String,
131    /// Docker network driver.
132    pub driver: String,
133    /// Whether the network is internal.
134    pub internal: bool,
135    /// Whether IPv6 is enabled.
136    pub enable_ipv6: bool,
137    /// Whether the network is attachable.
138    pub attachable: bool,
139    /// Network labels preserved during recreate.
140    pub labels: HashMap<String, String>,
141    /// Network options preserved during recreate.
142    pub options: HashMap<String, String>,
143    /// IPAM subnet configuration preserved during recreate.
144    pub ipam: Vec<NetworkIpamConfig>,
145    /// Whether the target network will be replaced.
146    pub replace_existing: bool,
147}
148
149/// A recreated container definition.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct ContainerPlan {
152    /// Source container name without the leading slash.
153    pub name: String,
154    /// Source container identifier.
155    pub id: String,
156    /// Image reference used when recreating the container.
157    pub image_reference: String,
158    /// Normalized container creation spec.
159    pub spec: ContainerSpec,
160    /// Additional network attachments after create.
161    pub extra_networks: Vec<ContainerNetworkAttachment>,
162    /// Whether the target container will be replaced.
163    pub replace_existing: bool,
164    /// Whether the container was running on the source, and so should be
165    /// started once the migration finishes.
166    pub was_running: bool,
167    /// Source creation timestamp, used to recreate in the original order.
168    pub created: String,
169}
170
171/// Container creation spec translated from inspect output.
172#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
173pub struct ContainerSpec {
174    /// Hostname.
175    pub hostname: Option<String>,
176    /// Domain name.
177    pub domainname: Option<String>,
178    /// User.
179    pub user: Option<String>,
180    /// Environment variables.
181    pub env: Vec<String>,
182    /// Labels.
183    pub labels: HashMap<String, String>,
184    /// Exposed ports.
185    pub exposed_ports: Vec<String>,
186    /// Whether tty mode is enabled.
187    pub tty: bool,
188    /// Whether stdin should stay open.
189    pub open_stdin: bool,
190    /// Working directory.
191    pub working_dir: Option<String>,
192    /// Entrypoint argv.
193    pub entrypoint: Vec<String>,
194    /// Command argv.
195    pub cmd: Vec<String>,
196    /// Mount definitions.
197    pub mounts: Vec<ContainerMount>,
198    /// Port publish rules.
199    pub publishes: Vec<PortPublish>,
200    /// Restart policy.
201    pub restart_policy: Option<RestartPolicySpec>,
202    /// Privileged mode.
203    pub privileged: bool,
204    /// Read-only root filesystem.
205    pub read_only_rootfs: bool,
206    /// Extra hosts.
207    pub extra_hosts: Vec<String>,
208    /// Auto-remove on exit.
209    pub auto_remove: bool,
210    /// Memory limit in bytes, when one is set.
211    pub memory: Option<i64>,
212    /// CPU quota in units of 10^-9 CPUs, when one is set.
213    pub nano_cpus: Option<i64>,
214    /// Added Linux capabilities.
215    pub cap_add: Vec<String>,
216    /// Network the container joins at create time.
217    pub network_mode: NetworkModeSpec,
218}
219
220/// The network a container joins at create time, from `HostConfig.NetworkMode`.
221///
222/// `container:<name|id>` is deliberately absent: it is rejected during planning
223/// rather than modelled, because reproducing it would require resolving the
224/// peer container and ordering creation around it.
225#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
226pub enum NetworkModeSpec {
227    /// The default bridge network; no `--network` flag is emitted.
228    #[default]
229    Default,
230    /// Host networking.
231    Host,
232    /// No networking.
233    None,
234    /// A user-defined network that is part of this migration.
235    Named(ContainerNetworkAttachment),
236}
237
238impl NetworkModeSpec {
239    /// Returns whether this mode shares another namespace, which bars the
240    /// container from joining any further network.
241    ///
242    /// Docker rejects that combination outright ("container sharing network
243    /// namespace with another container or host cannot be connected to any
244    /// other network"), so additional attachments must be skipped.
245    #[must_use]
246    pub const fn forbids_extra_networks(&self) -> bool {
247        matches!(self, Self::Host)
248    }
249}
250
251/// Supported mount definitions.
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
253pub enum ContainerMount {
254    /// Named volume mount.
255    Volume {
256        /// Volume name.
257        source: String,
258        /// Container destination path.
259        target: String,
260        /// Whether the mount is writable.
261        rw: bool,
262    },
263    /// Bind mount.
264    Bind {
265        /// Host path.
266        source: String,
267        /// Container destination path.
268        target: String,
269        /// Whether the mount is writable.
270        rw: bool,
271    },
272    /// Tmpfs mount.
273    Tmpfs {
274        /// Container destination path.
275        target: String,
276        /// Mount options string.
277        options: Option<String>,
278    },
279}
280
281/// Host port publish rule.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283pub struct PortPublish {
284    /// Port and protocol inside the container.
285    pub container_port: String,
286    /// Host IP, if present.
287    pub host_ip: Option<String>,
288    /// Host port.
289    pub host_port: Option<String>,
290}
291
292/// Restart policy.
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294pub struct RestartPolicySpec {
295    /// Policy name.
296    pub name: String,
297    /// Maximum retry count.
298    pub maximum_retry_count: Option<i64>,
299}
300
301/// Container network attachment.
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub struct ContainerNetworkAttachment {
304    /// Network name.
305    pub network: String,
306    /// Network-scoped aliases.
307    pub aliases: Vec<String>,
308}
309
310/// Aggregate replacement summary.
311#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
312pub struct ReplacementSummary {
313    /// Container names that will be replaced.
314    pub containers: Vec<String>,
315    /// Volume names that will be replaced.
316    pub volumes: Vec<String>,
317    /// Network names that will be replaced.
318    pub networks: Vec<String>,
319    /// Image tags that will be overwritten.
320    pub image_tags: Vec<String>,
321}
322
323impl ReplacementSummary {
324    /// Returns true when no replace action is required.
325    #[must_use]
326    pub fn is_empty(&self) -> bool {
327        self.containers.is_empty()
328            && self.volumes.is_empty()
329            && self.networks.is_empty()
330            && self.image_tags.is_empty()
331    }
332}
333
334/// A source volume blocked by running containers.
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336pub struct RunningVolumeBlocker {
337    /// Volume name.
338    pub volume_name: String,
339    /// Running source containers using the volume.
340    pub containers: Vec<String>,
341}