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}