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