use axum::http::Uri;
use bytes::Bytes;
use serde_json::Value;
pub use arcbox_core::UtilityVmRole;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeTranslator {
Native,
Fex,
}
impl RuntimeTranslator {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Native => "native",
Self::Fex => "fex",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkloadPlatform {
LinuxArm64,
LinuxAmd64,
Unspecified,
}
impl WorkloadPlatform {
#[must_use]
pub fn parse(platform: &str) -> Self {
let normalized = platform.trim().to_ascii_lowercase().replace("%2f", "/");
match normalized.as_str() {
"linux/amd64" | "linux/x86_64" | "amd64" | "x86_64" => Self::LinuxAmd64,
"linux/arm64" | "linux/aarch64" | "arm64" | "aarch64" => Self::LinuxArm64,
_ => Self::Unspecified,
}
}
#[must_use]
pub const fn runtime_translator(self) -> RuntimeTranslator {
match self {
Self::LinuxAmd64 => RuntimeTranslator::Fex,
Self::LinuxArm64 | Self::Unspecified => RuntimeTranslator::Native,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RoutingDecision {
pub platform: WorkloadPlatform,
pub translator: RuntimeTranslator,
}
impl RoutingDecision {
#[must_use]
pub const fn from_platform(platform: WorkloadPlatform) -> Self {
Self {
platform,
translator: platform.runtime_translator(),
}
}
#[must_use]
pub const fn native_default() -> Self {
Self::from_platform(WorkloadPlatform::Unspecified)
}
#[must_use]
pub const fn utility_vm(self) -> UtilityVmRole {
UtilityVmRole::Native
}
#[must_use]
pub const fn needs_fex(self) -> bool {
matches!(self.translator, RuntimeTranslator::Fex)
}
}
#[must_use]
pub const fn is_admissible(decision: RoutingDecision, fex_available: bool) -> bool {
match decision.translator {
RuntimeTranslator::Native => true,
RuntimeTranslator::Fex => fex_available,
}
}
#[must_use]
pub fn route_container_create(uri: &Uri, body: &Bytes) -> RoutingDecision {
let platform = platform_from_query(uri)
.or_else(|| platform_from_create_body(body))
.unwrap_or(WorkloadPlatform::Unspecified);
RoutingDecision::from_platform(platform)
}
#[must_use]
pub fn route_build(uri: &Uri) -> RoutingDecision {
RoutingDecision::from_platform(
platform_from_query(uri).unwrap_or(WorkloadPlatform::Unspecified),
)
}
fn platform_from_query(uri: &Uri) -> Option<WorkloadPlatform> {
query_param(uri, "platform").map(WorkloadPlatform::parse)
}
#[must_use]
pub fn query_param<'a>(uri: &'a Uri, key: &str) -> Option<&'a str> {
uri.query()?.split('&').find_map(|pair| {
let (k, v) = pair.split_once('=')?;
(k.eq_ignore_ascii_case(key) && !v.is_empty()).then_some(v)
})
}
fn platform_from_create_body(body: &Bytes) -> Option<WorkloadPlatform> {
let value: Value = serde_json::from_slice(body).ok()?;
let platform = value.get("Platform")?.as_str()?;
Some(WorkloadPlatform::parse(platform))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_amd64_platform_aliases() {
assert_eq!(
WorkloadPlatform::parse("linux/amd64"),
WorkloadPlatform::LinuxAmd64
);
assert_eq!(
WorkloadPlatform::parse("linux/x86_64"),
WorkloadPlatform::LinuxAmd64
);
assert_eq!(
WorkloadPlatform::parse("amd64"),
WorkloadPlatform::LinuxAmd64
);
}
#[test]
fn parses_arm64_platform_aliases() {
assert_eq!(
WorkloadPlatform::parse("linux/arm64"),
WorkloadPlatform::LinuxArm64
);
assert_eq!(
WorkloadPlatform::parse("linux/aarch64"),
WorkloadPlatform::LinuxArm64
);
assert_eq!(
WorkloadPlatform::parse("aarch64"),
WorkloadPlatform::LinuxArm64
);
}
#[test]
fn amd64_selects_fex_translator_on_hv() {
let route = RoutingDecision::from_platform(WorkloadPlatform::LinuxAmd64);
assert_eq!(route.translator, RuntimeTranslator::Fex);
assert_eq!(route.utility_vm(), UtilityVmRole::Native);
assert!(route.needs_fex());
}
#[test]
fn arm64_and_unspecified_select_native_translator() {
for platform in [WorkloadPlatform::LinuxArm64, WorkloadPlatform::Unspecified] {
let route = RoutingDecision::from_platform(platform);
assert_eq!(route.translator, RuntimeTranslator::Native);
assert_eq!(route.utility_vm(), UtilityVmRole::Native);
assert!(!route.needs_fex());
}
}
#[test]
fn amd64_admitted_only_when_fex_available() {
let amd64 = RoutingDecision::from_platform(WorkloadPlatform::LinuxAmd64);
assert!(
!is_admissible(amd64, false),
"amd64 must fail closed without FEX"
);
assert!(is_admissible(amd64, true));
}
#[test]
fn native_workloads_always_admissible() {
let arm64 = RoutingDecision::from_platform(WorkloadPlatform::LinuxArm64);
let unspec = RoutingDecision::native_default();
assert!(is_admissible(arm64, false));
assert!(is_admissible(unspec, false));
}
#[test]
fn container_create_prefers_query_platform_over_body() {
let uri = "/containers/create?platform=linux/amd64".parse().unwrap();
let body = Bytes::from_static(br#"{"Image":"alpine","Platform":"linux/arm64"}"#);
let route = route_container_create(&uri, &body);
assert_eq!(route.platform, WorkloadPlatform::LinuxAmd64);
assert_eq!(route.translator, RuntimeTranslator::Fex);
}
#[test]
fn container_create_uses_body_platform_when_query_absent() {
let uri = "/containers/create".parse().unwrap();
let body = Bytes::from_static(br#"{"Image":"alpine","Platform":"linux/amd64"}"#);
let route = route_container_create(&uri, &body);
assert_eq!(route.translator, RuntimeTranslator::Fex);
}
#[test]
fn build_uses_query_platform() {
let uri = "/build?t=image&platform=linux%2Famd64".parse().unwrap();
let route = route_build(&uri);
assert_eq!(route.platform, WorkloadPlatform::LinuxAmd64);
assert_eq!(route.translator, RuntimeTranslator::Fex);
}
}