Skip to main content

microsandbox_migration/
schema_metadata.rs

1//! Static metadata for downgrade planning.
2//!
3//! `Migrator::migrations()` owns the executable migration order. This module
4//! keeps the user-facing downgrade metadata in the same crate so release checks
5//! can ensure every migration has an explicit reversibility and cache-impact
6//! decision before a new binary ships.
7
8use std::collections::HashSet;
9
10//--------------------------------------------------------------------------------------------------
11// Constants
12//--------------------------------------------------------------------------------------------------
13
14/// Version of the hidden schema-baseline JSON shape emitted by the CLI.
15pub const SCHEMA_BASELINE_FORMAT_VERSION: u32 = 1;
16
17/// Oldest release supported by the downgrade flow.
18pub const DOWNGRADE_FLOOR: &str = "0.6.0";
19
20/// Migration that introduced the DB-backed maintenance lease table.
21pub const MAINTENANCE_LEASE_MIGRATION_ID: &str = "m20260621_000002_create_maintenance_lease";
22
23/// Migration that introduced desired-vs-active sandbox config tracking.
24pub const ACTIVE_CONFIG_MIGRATION_ID: &str = "m20260703_000001_add_sandbox_active_config";
25
26/// Migration that adds payload scope metadata to the snapshot index.
27pub const SNAPSHOT_SCOPE_MIGRATION_ID: &str = "m20260714_000001_add_snapshot_scope";
28
29/// Migration that projects final snapshot state and journals legacy conversion.
30pub const SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID: &str =
31    "m20260723_000001_snapshot_artifact_transition";
32
33/// Migration that introduces cooperative host CPU allocation state.
34pub const CPU_ALLOCATION_MIGRATION_ID: &str = "m20260719_000001_create_cpu_allocations";
35
36/// Migration that introduces host-global writeback dirty-credit reservations.
37pub const WRITEBACK_ALLOCATION_MIGRATION_ID: &str = "m20260803_000001_create_writeback_allocations";
38
39/// Migration that records per-NUMA-node guest memory promises for CPU allocations.
40pub const MEMORY_ALLOCATION_NODES_MIGRATION_ID: &str =
41    "m20260808_000001_create_memory_allocation_nodes";
42
43/// Migration that rebuilds the sandbox label index from persisted configs.
44pub const SANDBOX_LABEL_REBUILD_MIGRATION_ID: &str = "m20260810_000001_rebuild_sandbox_labels";
45
46/// Migration that permits several managed vCPUs to share one host logical processor.
47pub const SHARED_CPU_ALLOCATION_MIGRATION_ID: &str = "m20260813_000001_share_cpu_allocations";
48/// Migration that adds recyclable network address-pool slot leases.
49pub const SANDBOX_NETWORK_SLOT_MIGRATION_ID: &str = "m20260818_000001_sandbox_network_slot";
50
51/// Migration that prevents old binaries from discarding persisted mount ownership.
52pub const MOUNT_OWNER_CONFIG_MIGRATION_ID: &str = "m20260824_000001_mount_owner_config";
53
54/// Migration that separates stable snapshot identity from descriptor integrity.
55pub const SNAPSHOT_IDENTITY_MIGRATION_ID: &str = "m20260829_000001_split_snapshot_identity";
56
57/// Migration that separates local group membership from portable snapshot identity.
58pub const SNAPSHOT_GROUPS_MIGRATION_ID: &str = "m20260910_000001_snapshot_groups";
59
60/// Normalizes saved secret policies to the current representation.
61pub const SECRET_CONFIG_MIGRATION_ID: &str = "m20260922_000001_migrate_secret_config";
62
63/// Migration that prevents old binaries from discarding the guest clock policy.
64pub const GUEST_CLOCK_CONFIG_MIGRATION_ID: &str = "m20261001_000001_guest_clock_config";
65
66/// Frozen migration baseline for the transitional 0.6.0 release.
67///
68/// The released 0.6.0 binary predates `msb __schema-baseline --json`, so
69/// downgrade uses this fixture when inspecting that exact target. Do not extend
70/// this list when adding later migrations; future targets should answer with
71/// their own hidden baseline command.
72pub 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
88/// Metadata for every migration in `Migrator::migrations()` order.
89pub 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        // This backdated migration first shipped in v0.6.16. Keep it after
260        // the v0.6.15 mount-owner marker so released databases stay prefixes.
261        id: SANDBOX_NETWORK_SLOT_MIGRATION_ID,
262        // The column is deliberately left in place on rollback (SQLite has
263        // no DROP COLUMN on every supported version); `up` probes for it so a
264        // re-upgrade after this rollback succeeds.
265        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//--------------------------------------------------------------------------------------------------
301// Types
302//--------------------------------------------------------------------------------------------------
303
304/// Downgrade metadata for one migration.
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub struct MigrationMetadata {
307    /// Migration identifier returned by `MigrationName::name()`.
308    pub id: &'static str,
309
310    /// Whether `down()` actually restores a target-compatible schema/state.
311    pub reversible: bool,
312
313    /// Whether rolling this migration back invalidates re-pullable image cache
314    /// contents on disk.
315    pub affects_cache: bool,
316
317    /// Whether rolling this migration back may leave snapshots or disk-backed
318    /// named volumes in a format the target release cannot read.
319    pub affects_user_data: bool,
320
321    /// Short human-readable summary used in destructive downgrade prompts.
322    pub summary: &'static str,
323}
324
325//--------------------------------------------------------------------------------------------------
326// Functions
327//--------------------------------------------------------------------------------------------------
328
329/// Return all migration identifiers in schema order.
330pub fn migration_ids() -> impl Iterator<Item = &'static str> {
331    MIGRATION_METADATA.iter().map(|metadata| metadata.id)
332}
333
334/// Resolve an unordered collection of applied migration identifiers to its canonical prefix.
335///
336/// SeaORM records migration timestamps with insufficient precision to recover execution order
337/// when several migrations run together. Treat the migration table as a set and use this binary's
338/// append-only metadata as the only source of ordering instead.
339pub 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    // Duplicate migration rows are invalid even if their distinct identifiers resemble a prefix.
349    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//--------------------------------------------------------------------------------------------------
361// Tests
362//--------------------------------------------------------------------------------------------------
363
364#[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        // This is also main's complete prefix at the v0.7.0 integration point.
436        // Keep execution order, not timestamp order: every main migration must
437        // precede the unreleased snapshot migrations when the branches converge.
438        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            &current[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}