1use std::collections::HashSet;
9
10pub const SCHEMA_BASELINE_FORMAT_VERSION: u32 = 1;
16
17pub const DOWNGRADE_FLOOR: &str = "0.6.0";
19
20pub const MAINTENANCE_LEASE_MIGRATION_ID: &str = "m20260621_000002_create_maintenance_lease";
22
23pub const ACTIVE_CONFIG_MIGRATION_ID: &str = "m20260703_000001_add_sandbox_active_config";
25
26pub const SNAPSHOT_SCOPE_MIGRATION_ID: &str = "m20260714_000001_add_snapshot_scope";
28
29pub const SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID: &str =
31 "m20260723_000001_snapshot_artifact_transition";
32
33pub const CPU_ALLOCATION_MIGRATION_ID: &str = "m20260719_000001_create_cpu_allocations";
35
36pub const WRITEBACK_ALLOCATION_MIGRATION_ID: &str = "m20260803_000001_create_writeback_allocations";
38
39pub const MEMORY_ALLOCATION_NODES_MIGRATION_ID: &str =
41 "m20260808_000001_create_memory_allocation_nodes";
42
43pub const SANDBOX_LABEL_REBUILD_MIGRATION_ID: &str = "m20260810_000001_rebuild_sandbox_labels";
45
46pub const SHARED_CPU_ALLOCATION_MIGRATION_ID: &str = "m20260813_000001_share_cpu_allocations";
48pub const SANDBOX_NETWORK_SLOT_MIGRATION_ID: &str = "m20260818_000001_sandbox_network_slot";
50
51pub const MOUNT_OWNER_CONFIG_MIGRATION_ID: &str = "m20260824_000001_mount_owner_config";
53
54pub const SNAPSHOT_IDENTITY_MIGRATION_ID: &str = "m20260829_000001_split_snapshot_identity";
56
57pub const SNAPSHOT_GROUPS_MIGRATION_ID: &str = "m20260910_000001_snapshot_groups";
59
60pub const SECRET_CONFIG_MIGRATION_ID: &str = "m20260922_000001_migrate_secret_config";
62
63pub const GUEST_CLOCK_CONFIG_MIGRATION_ID: &str = "m20261001_000001_guest_clock_config";
65
66pub const SANDBOX_CALLER_CONFIG_OVERRIDES_MIGRATION_ID: &str =
68 "m20261022_000001_sandbox_caller_config_overrides";
69
70pub const BASELINE_0_6_0_MIGRATIONS: &[&str] = &[
77 "m20260305_000001_create_image_tables",
78 "m20260305_000002_create_sandbox_tables",
79 "m20260305_000003_create_storage_tables",
80 "m20260305_000004_create_sandbox_images_table",
81 "m20260410_000001_erofs_image_schema",
82 "m20260501_000001_create_snapshot_index",
83 "m20260517_000001_drop_sandbox_metric",
84 "m20260527_000001_migrate_oci_rootfs_source",
85 "m20260531_000001_create_sandbox_labels",
86 "m20260531_000002_index_sandbox_labels_key_value",
87 "m20260606_000001_named_volume_kinds",
88 "m20260621_000001_add_sandbox_ephemeral",
89 MAINTENANCE_LEASE_MIGRATION_ID,
90];
91
92pub const MIGRATION_METADATA: &[MigrationMetadata] = &[
94 MigrationMetadata {
95 id: "m20260305_000001_create_image_tables",
96 reversible: true,
97 affects_cache: true,
98 affects_user_data: false,
99 summary: "remove legacy OCI image catalog tables",
100 },
101 MigrationMetadata {
102 id: "m20260305_000002_create_sandbox_tables",
103 reversible: true,
104 affects_cache: false,
105 affects_user_data: false,
106 summary: "remove sandbox and run tables",
107 },
108 MigrationMetadata {
109 id: "m20260305_000003_create_storage_tables",
110 reversible: true,
111 affects_cache: false,
112 affects_user_data: false,
113 summary: "remove volume and snapshot storage tables",
114 },
115 MigrationMetadata {
116 id: "m20260305_000004_create_sandbox_images_table",
117 reversible: true,
118 affects_cache: true,
119 affects_user_data: false,
120 summary: "remove sandbox image references",
121 },
122 MigrationMetadata {
123 id: "m20260410_000001_erofs_image_schema",
124 reversible: true,
125 affects_cache: true,
126 affects_user_data: false,
127 summary: "remove EROFS rootfs catalog tables",
128 },
129 MigrationMetadata {
130 id: "m20260501_000001_create_snapshot_index",
131 reversible: true,
132 affects_cache: false,
133 affects_user_data: false,
134 summary: "remove snapshot index table",
135 },
136 MigrationMetadata {
137 id: "m20260517_000001_drop_sandbox_metric",
138 reversible: false,
139 affects_cache: false,
140 affects_user_data: false,
141 summary: "restore legacy sandbox metrics table",
142 },
143 MigrationMetadata {
144 id: "m20260527_000001_migrate_oci_rootfs_source",
145 reversible: false,
146 affects_cache: false,
147 affects_user_data: false,
148 summary: "rewrite OCI rootfs config back to the legacy string shape",
149 },
150 MigrationMetadata {
151 id: "m20260531_000001_create_sandbox_labels",
152 reversible: true,
153 affects_cache: false,
154 affects_user_data: false,
155 summary: "remove sandbox labels table",
156 },
157 MigrationMetadata {
158 id: "m20260531_000002_index_sandbox_labels_key_value",
159 reversible: true,
160 affects_cache: false,
161 affects_user_data: false,
162 summary: "remove sandbox label key/value index",
163 },
164 MigrationMetadata {
165 id: "m20260606_000001_named_volume_kinds",
166 reversible: true,
167 affects_cache: false,
168 affects_user_data: false,
169 summary: "remove named volume kind columns and attachments",
170 },
171 MigrationMetadata {
172 id: "m20260621_000001_add_sandbox_ephemeral",
173 reversible: true,
174 affects_cache: false,
175 affects_user_data: false,
176 summary: "remove sandbox ephemeral flag",
177 },
178 MigrationMetadata {
179 id: MAINTENANCE_LEASE_MIGRATION_ID,
180 reversible: true,
181 affects_cache: false,
182 affects_user_data: false,
183 summary: "remove maintenance lease table",
184 },
185 MigrationMetadata {
186 id: ACTIVE_CONFIG_MIGRATION_ID,
187 reversible: true,
188 affects_cache: false,
189 affects_user_data: false,
190 summary: "remove active sandbox config snapshots",
191 },
192 MigrationMetadata {
193 id: "m20260708_000001_migrate_bind_rootfs_source",
194 reversible: true,
195 affects_cache: false,
196 affects_user_data: true,
197 summary: "rewrite bind rootfs config back to the legacy string shape",
198 },
199 MigrationMetadata {
200 id: "m20260710_000001_migrate_root_disk",
201 reversible: true,
202 affects_cache: false,
203 affects_user_data: true,
204 summary: "rewrite root disk config back to the upper size shape",
205 },
206 MigrationMetadata {
207 id: SNAPSHOT_SCOPE_MIGRATION_ID,
208 reversible: true,
209 affects_cache: false,
210 affects_user_data: false,
211 summary: "remove snapshot scope index metadata",
212 },
213 MigrationMetadata {
214 id: SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID,
215 reversible: true,
216 affects_cache: false,
217 affects_user_data: true,
218 summary: "reverse final snapshot descriptors before removing migration state",
219 },
220 MigrationMetadata {
221 id: CPU_ALLOCATION_MIGRATION_ID,
222 reversible: true,
223 affects_cache: false,
224 affects_user_data: false,
225 summary: "remove cooperative host CPU allocation tables",
226 },
227 MigrationMetadata {
228 id: WRITEBACK_ALLOCATION_MIGRATION_ID,
229 reversible: true,
230 affects_cache: false,
231 affects_user_data: false,
232 summary: "remove host-global writeback allocation state",
233 },
234 MigrationMetadata {
235 id: MEMORY_ALLOCATION_NODES_MIGRATION_ID,
236 reversible: true,
237 affects_cache: false,
238 affects_user_data: false,
239 summary: "remove cooperative NUMA memory allocation state",
240 },
241 MigrationMetadata {
242 id: SANDBOX_LABEL_REBUILD_MIGRATION_ID,
243 reversible: true,
244 affects_cache: false,
245 affects_user_data: false,
246 summary: "retain the rebuilt sandbox label index",
247 },
248 MigrationMetadata {
249 id: SHARED_CPU_ALLOCATION_MIGRATION_ID,
250 reversible: true,
251 affects_cache: false,
252 affects_user_data: false,
253 summary: "restore exclusive logical CPU allocation rows",
254 },
255 MigrationMetadata {
256 id: MOUNT_OWNER_CONFIG_MIGRATION_ID,
257 reversible: true,
258 affects_cache: false,
259 affects_user_data: false,
260 summary: "remove the compatibility marker after confirming no persisted mount ownership",
261 },
262 MigrationMetadata {
263 id: SANDBOX_NETWORK_SLOT_MIGRATION_ID,
266 reversible: true,
270 affects_cache: false,
271 affects_user_data: false,
272 summary: "retain the compatible sandbox network slot column",
273 },
274 MigrationMetadata {
275 id: SNAPSHOT_IDENTITY_MIGRATION_ID,
276 reversible: true,
277 affects_cache: true,
278 affects_user_data: true,
279 summary: "reverse final snapshot descriptors before dropping identity projections",
280 },
281 MigrationMetadata {
282 id: SNAPSHOT_GROUPS_MIGRATION_ID,
283 reversible: true,
284 affects_cache: false,
285 affects_user_data: true,
286 summary: "restore the flat snapshot index only when no groups or duplicate identities remain",
287 },
288 MigrationMetadata {
289 id: SECRET_CONFIG_MIGRATION_ID,
290 reversible: true,
291 affects_cache: false,
292 affects_user_data: false,
293 summary: "retain secret policies only when the target can represent them",
294 },
295 MigrationMetadata {
296 id: GUEST_CLOCK_CONFIG_MIGRATION_ID,
297 reversible: true,
298 affects_cache: false,
299 affects_user_data: false,
300 summary: "remove the compatibility marker only when no sandbox disables guest clock sync",
301 },
302 MigrationMetadata {
303 id: SANDBOX_CALLER_CONFIG_OVERRIDES_MIGRATION_ID,
304 reversible: true,
305 affects_cache: false,
306 affects_user_data: true,
307 summary: "drop sparse caller intent while keeping resolved sandbox configuration",
308 },
309];
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub struct MigrationMetadata {
318 pub id: &'static str,
320
321 pub reversible: bool,
323
324 pub affects_cache: bool,
327
328 pub affects_user_data: bool,
331
332 pub summary: &'static str,
334}
335
336pub fn migration_ids() -> impl Iterator<Item = &'static str> {
342 MIGRATION_METADATA.iter().map(|metadata| metadata.id)
343}
344
345pub fn canonical_applied_prefix<'a>(
351 applied_ids: impl IntoIterator<Item = &'a str>,
352) -> Option<&'static [MigrationMetadata]> {
353 let mut applied_count = 0;
354 let applied_ids: HashSet<_> = applied_ids
355 .into_iter()
356 .inspect(|_| applied_count += 1)
357 .collect();
358
359 if applied_ids.len() != applied_count {
361 return None;
362 }
363
364 let prefix = MIGRATION_METADATA.get(..applied_count)?;
365 prefix
366 .iter()
367 .all(|metadata| applied_ids.contains(metadata.id))
368 .then_some(prefix)
369}
370
371#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::{Migrator, MigratorTrait};
379
380 #[test]
381 fn metadata_matches_migrator_order() {
382 let migrations = Migrator::migrations();
383 let migrator_ids: Vec<_> = migrations
384 .iter()
385 .map(|migration| migration.name().to_string())
386 .collect();
387 let metadata_ids: Vec<_> = migration_ids().map(str::to_string).collect();
388
389 assert_eq!(metadata_ids, migrator_ids);
390 }
391
392 #[test]
393 fn canonical_applied_prefix_uses_metadata_order() {
394 let applied = [
395 SANDBOX_CALLER_CONFIG_OVERRIDES_MIGRATION_ID,
396 GUEST_CLOCK_CONFIG_MIGRATION_ID,
397 SECRET_CONFIG_MIGRATION_ID,
398 SNAPSHOT_GROUPS_MIGRATION_ID,
399 SNAPSHOT_IDENTITY_MIGRATION_ID,
400 MOUNT_OWNER_CONFIG_MIGRATION_ID,
401 SANDBOX_NETWORK_SLOT_MIGRATION_ID,
402 SHARED_CPU_ALLOCATION_MIGRATION_ID,
403 SANDBOX_LABEL_REBUILD_MIGRATION_ID,
404 MEMORY_ALLOCATION_NODES_MIGRATION_ID,
405 WRITEBACK_ALLOCATION_MIGRATION_ID,
406 SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID,
407 CPU_ALLOCATION_MIGRATION_ID,
408 ];
409 let prefix_len = MIGRATION_METADATA.len();
410 let mut all_applied: Vec<_> = MIGRATION_METADATA[..prefix_len - applied.len()]
411 .iter()
412 .map(|metadata| metadata.id)
413 .collect();
414 all_applied.extend(applied);
415
416 let prefix = canonical_applied_prefix(all_applied).expect("valid unordered prefix");
417 assert_eq!(prefix, MIGRATION_METADATA);
418 }
419
420 #[test]
421 fn canonical_applied_prefix_rejects_gaps_and_unknown_migrations() {
422 let without_first = MIGRATION_METADATA
423 .iter()
424 .skip(1)
425 .map(|metadata| metadata.id);
426 assert!(canonical_applied_prefix(without_first).is_none());
427
428 let with_unknown = MIGRATION_METADATA
429 .iter()
430 .map(|metadata| metadata.id)
431 .chain(["m20990101_000001_future"]);
432 assert!(canonical_applied_prefix(with_unknown).is_none());
433 }
434
435 #[test]
436 fn released_v0_6_15_migrations_remain_a_prefix() {
437 let applied: Vec<_> = migration_ids()
438 .take_while(|id| *id != SANDBOX_NETWORK_SLOT_MIGRATION_ID)
439 .collect();
440
441 assert_eq!(applied.last(), Some(&MOUNT_OWNER_CONFIG_MIGRATION_ID));
442 assert!(canonical_applied_prefix(applied).is_some());
443 }
444
445 #[test]
446 fn snapshot_stack_follows_released_v0_6_18_prefix() {
447 let released = [
451 "m20260305_000001_create_image_tables",
452 "m20260305_000002_create_sandbox_tables",
453 "m20260305_000003_create_storage_tables",
454 "m20260305_000004_create_sandbox_images_table",
455 "m20260410_000001_erofs_image_schema",
456 "m20260501_000001_create_snapshot_index",
457 "m20260517_000001_drop_sandbox_metric",
458 "m20260527_000001_migrate_oci_rootfs_source",
459 "m20260531_000001_create_sandbox_labels",
460 "m20260531_000002_index_sandbox_labels_key_value",
461 "m20260606_000001_named_volume_kinds",
462 "m20260621_000001_add_sandbox_ephemeral",
463 "m20260621_000002_create_maintenance_lease",
464 "m20260703_000001_add_sandbox_active_config",
465 "m20260708_000001_migrate_bind_rootfs_source",
466 "m20260710_000001_migrate_root_disk",
467 "m20260714_000001_add_snapshot_scope",
468 "m20260723_000001_snapshot_artifact_transition",
469 "m20260719_000001_create_cpu_allocations",
470 "m20260803_000001_create_writeback_allocations",
471 "m20260808_000001_create_memory_allocation_nodes",
472 "m20260810_000001_rebuild_sandbox_labels",
473 "m20260813_000001_share_cpu_allocations",
474 "m20260824_000001_mount_owner_config",
475 "m20260818_000001_sandbox_network_slot",
476 ];
477 let current: Vec<_> = migration_ids().collect();
478 assert!(current.starts_with(&released));
479 assert_eq!(
480 ¤t[released.len()..],
481 &[
482 SNAPSHOT_IDENTITY_MIGRATION_ID,
483 SNAPSHOT_GROUPS_MIGRATION_ID,
484 SECRET_CONFIG_MIGRATION_ID,
485 GUEST_CLOCK_CONFIG_MIGRATION_ID,
486 SANDBOX_CALLER_CONFIG_OVERRIDES_MIGRATION_ID,
487 ],
488 );
489 assert!(canonical_applied_prefix(released).is_some());
490 }
491
492 #[test]
493 fn frozen_0_6_0_baseline_is_current_prefix() {
494 let metadata_ids: Vec<_> = migration_ids().collect();
495 assert!(metadata_ids.starts_with(BASELINE_0_6_0_MIGRATIONS));
496 }
497}