use std::io::{Read as _, Seek as _, SeekFrom};
use std::path::Path;
use arcbox_constants::paths::{CONTAINERD_DATA_MOUNT_POINT, DOCKER_DATA_MOUNT_POINT};
use arcbox_constants::devices::DOCKER_METADATA_BLOCK_DEVICE;
use super::cmdline::declared_docker_metadata_device;
use crate::metadata_migrate::{EntryKind, Prepared, prepare_entry};
const METADATA_MOUNT: &str = "/run/arcbox/metadata";
const MKFS_EXT4: &str = "/sbin/mkfs.ext4";
const E2FSCK: &str = "/sbin/e2fsck";
const EXT4_MAGIC_OFFSET: u64 = 1024 + 56;
const EXT4_MAGIC: [u8; 2] = [0x53, 0xEF];
struct Mapping {
name: &'static str,
target: String,
kind: EntryKind,
}
fn mappings() -> Vec<Mapping> {
vec![
Mapping {
name: "containerd-bolt",
target: format!("{CONTAINERD_DATA_MOUNT_POINT}/io.containerd.metadata.v1.bolt"),
kind: EntryKind::Dir,
},
Mapping {
name: "snapshotter-metadata.db",
target: format!(
"{CONTAINERD_DATA_MOUNT_POINT}/io.containerd.snapshotter.v1.overlayfs/metadata.db"
),
kind: EntryKind::File,
},
Mapping {
name: "docker-network",
target: format!("{DOCKER_DATA_MOUNT_POINT}/network"),
kind: EntryKind::Dir,
},
Mapping {
name: "docker-image",
target: format!("{DOCKER_DATA_MOUNT_POINT}/image"),
kind: EntryKind::Dir,
},
Mapping {
name: "docker-buildkit",
target: format!("{DOCKER_DATA_MOUNT_POINT}/buildkit"),
kind: EntryKind::Dir,
},
]
}
pub(super) fn ensure_metadata_mount() -> Result<String, String> {
let maps = mappings();
if maps.iter().all(|m| crate::mount::is_mounted(&m.target)) {
return Ok("metadata binds already mounted".to_string());
}
let device = match declared_docker_metadata_device() {
Some(device) => {
if !wait_for_device(&device) {
return Err(format!("declared metadata device {device} never appeared"));
}
device
}
None if Path::new(DOCKER_METADATA_BLOCK_DEVICE).exists() => {
DOCKER_METADATA_BLOCK_DEVICE.to_string()
}
None => {
tracing::warn!("no metadata device declared or present; btrfs-only layout");
return Ok("metadata volume skipped (no device)".to_string());
}
};
let mut notes = Vec::new();
if !has_ext4_superblock(&device) {
if !Path::new(MKFS_EXT4).exists() {
tracing::warn!("mkfs.ext4 missing and metadata device blank; skipping metadata volume");
return Ok("metadata volume skipped (no mkfs.ext4)".to_string());
}
notes.push(format_ext4(&device)?);
}
mount_metadata(&device, &mut notes)?;
for mapping in &maps {
if crate::mount::is_mounted(&mapping.target) {
continue;
}
let volume_entry = Path::new(METADATA_MOUNT).join(mapping.name);
match prepare_entry(
Path::new(METADATA_MOUNT),
Path::new(&mapping.target),
mapping.name,
mapping.kind,
) {
Ok(Prepared::Migrated) => notes.push(format!("migrated {}", mapping.target)),
Ok(_) => {}
Err(e) => return Err(format!("prepare {} failed: {e}", mapping.target)),
}
bind(&volume_entry, &mapping.target)?;
}
if notes.is_empty() {
Ok("metadata volume mounted".to_string())
} else {
Ok(notes.join("; "))
}
}
fn wait_for_device(device: &str) -> bool {
for attempt in 0..50 {
if Path::new(device).exists() {
if attempt > 0 {
tracing::info!(device, attempt, "waited for metadata device");
}
return true;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
false
}
fn has_ext4_superblock(device: &str) -> bool {
let Ok(mut file) = std::fs::File::open(device) else {
return false;
};
if file.seek(SeekFrom::Start(EXT4_MAGIC_OFFSET)).is_err() {
return false;
}
let mut magic = [0_u8; 2];
file.read_exact(&mut magic).is_ok() && magic == EXT4_MAGIC
}
fn format_ext4(device: &str) -> Result<String, String> {
match std::process::Command::new(MKFS_EXT4)
.args([
"-F",
"-t",
"ext4",
"-O",
"has_journal,extent,huge_file,flex_bg,metadata_csum,64bit,dir_nlink,extra_isize,fast_commit",
"-E",
"lazy_itable_init=0,lazy_journal_init=0",
"-L",
"arcbox-meta",
device,
])
.status()
{
Ok(status) if status.success() => Ok(format!("formatted {device} as ext4")),
Ok(status) => Err(format!(
"mkfs.ext4 failed on {device} (exit={})",
status.code().unwrap_or(-1)
)),
Err(e) => Err(format!("failed to execute mkfs.ext4: {e}")),
}
}
fn mount_metadata(device: &str, notes: &mut Vec<String>) -> Result<(), String> {
if crate::mount::is_mounted(METADATA_MOUNT) {
return Ok(());
}
std::fs::create_dir_all(METADATA_MOUNT)
.map_err(|e| format!("failed to create {METADATA_MOUNT}: {e}"))?;
if try_mount(device) {
return Ok(());
}
if !Path::new(E2FSCK).exists() {
return Err(format!(
"mount {device} on {METADATA_MOUNT} failed and {E2FSCK} is unavailable"
));
}
match std::process::Command::new(E2FSCK)
.args(["-y", device])
.status()
{
Ok(status) if status.code().is_some_and(|c| c <= 2) => {
notes.push(format!("e2fsck repaired {device}"));
}
Ok(status) => {
return Err(format!(
"e2fsck failed on {device} (exit={})",
status.code().unwrap_or(-1)
));
}
Err(e) => return Err(format!("failed to execute e2fsck: {e}")),
}
if try_mount(device) {
Ok(())
} else {
Err(format!(
"mount {device} on {METADATA_MOUNT} failed even after e2fsck"
))
}
}
fn try_mount(device: &str) -> bool {
matches!(
std::process::Command::new("/bin/busybox")
.args(["mount", "-t", "ext4", "-o", "noatime", device, METADATA_MOUNT])
.status(),
Ok(status) if status.success()
)
}
fn bind(source: &Path, target: &str) -> Result<(), String> {
nix::mount::mount(
Some(source),
target,
None::<&str>,
nix::mount::MsFlags::MS_BIND,
None::<&str>,
)
.map_err(|e| format!("bind {} -> {target} failed: {e}", source.display()))
}