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/// Migration that retains sparse caller overrides beside resolved sandbox settings.
67pub const SANDBOX_CALLER_CONFIG_OVERRIDES_MIGRATION_ID: &str =
68    "m20261022_000001_sandbox_caller_config_overrides";
69
70/// Frozen migration baseline for the transitional 0.6.0 release.
71///
72/// The released 0.6.0 binary predates `msb __schema-baseline --json`, so
73/// downgrade uses this fixture when inspecting that exact target. Do not extend
74/// this list when adding later migrations; future targets should answer with
75/// their own hidden baseline command.
76pub 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
92/// Metadata for every migration in `Migrator::migrations()` order.
93pub 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        // This backdated migration first shipped in v0.6.16. Keep it after
264        // the v0.6.15 mount-owner marker so released databases stay prefixes.
265        id: SANDBOX_NETWORK_SLOT_MIGRATION_ID,
266        // The column is deliberately left in place on rollback (SQLite has
267        // no DROP COLUMN on every supported version); `up` probes for it so a
268        // re-upgrade after this rollback succeeds.
269        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//--------------------------------------------------------------------------------------------------
312// Types
313//--------------------------------------------------------------------------------------------------
314
315/// Downgrade metadata for one migration.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub struct MigrationMetadata {
318    /// Migration identifier returned by `MigrationName::name()`.
319    pub id: &'static str,
320
321    /// Whether `down()` actually restores a target-compatible schema/state.
322    pub reversible: bool,
323
324    /// Whether rolling this migration back invalidates re-pullable image cache
325    /// contents on disk.
326    pub affects_cache: bool,
327
328    /// Whether rolling this migration back may leave snapshots or disk-backed
329    /// named volumes in a format the target release cannot read.
330    pub affects_user_data: bool,
331
332    /// Short human-readable summary used in destructive downgrade prompts.
333    pub summary: &'static str,
334}
335
336//--------------------------------------------------------------------------------------------------
337// Functions
338//--------------------------------------------------------------------------------------------------
339
340/// Return all migration identifiers in schema order.
341pub fn migration_ids() -> impl Iterator<Item = &'static str> {
342    MIGRATION_METADATA.iter().map(|metadata| metadata.id)
343}
344
345/// Resolve an unordered collection of applied migration identifiers to its canonical prefix.
346///
347/// SeaORM records migration timestamps with insufficient precision to recover execution order
348/// when several migrations run together. Treat the migration table as a set and use this binary's
349/// append-only metadata as the only source of ordering instead.
350pub 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    // Duplicate migration rows are invalid even if their distinct identifiers resemble a prefix.
360    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//--------------------------------------------------------------------------------------------------
372// Tests
373//--------------------------------------------------------------------------------------------------
374
375#[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        // This is also main's complete prefix at the v0.7.0 integration point.
448        // Keep execution order, not timestamp order: every main migration must
449        // precede the unreleased snapshot migrations when the branches converge.
450        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            &current[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}