use crate::api::AppState;
use crate::error::{DockerError, Result};
use crate::proxy;
use crate::routing::UtilityVmRole;
use crate::workload::WorkloadRoleLookup;
use axum::body::Body;
use axum::http::{Request, Uri};
use axum::response::Response;
macro_rules! proxy_handler {
($name:ident) => {
pub async fn $name(
axum::extract::State(state): axum::extract::State<crate::api::AppState>,
axum::extract::OriginalUri(uri): axum::extract::OriginalUri,
req: axum::http::Request<axum::body::Body>,
) -> crate::error::Result<axum::response::Response> {
$crate::handlers::proxy(&state, &uri, req).await
}
};
}
macro_rules! container_proxy_handler {
($name:ident) => {
pub async fn $name(
axum::extract::State(state): axum::extract::State<crate::api::AppState>,
axum::extract::OriginalUri(uri): axum::extract::OriginalUri,
req: axum::http::Request<axum::body::Body>,
) -> crate::error::Result<axum::response::Response> {
let role = $crate::handlers::resolve_container_role(&state, &uri).await?;
$crate::handlers::proxy_to_role(&state, role, &uri, req).await
}
};
}
macro_rules! exec_proxy_handler {
($name:ident) => {
pub async fn $name(
axum::extract::State(state): axum::extract::State<crate::api::AppState>,
axum::extract::OriginalUri(uri): axum::extract::OriginalUri,
req: axum::http::Request<axum::body::Body>,
) -> crate::error::Result<axum::response::Response> {
let role = $crate::handlers::resolve_exec_role(&state, &uri).await?;
$crate::handlers::proxy_to_role(&state, role, &uri, req).await
}
};
}
pub(crate) use {container_proxy_handler, exec_proxy_handler, proxy_handler};
#[must_use]
pub(crate) fn extract_container_id(uri: &Uri) -> Option<String> {
extract_id_after_segment(uri, "containers", &["json", "create", "prune"])
}
#[must_use]
pub(crate) fn extract_exec_id(uri: &Uri) -> Option<String> {
extract_id_after_segment(uri, "exec", &[])
}
fn extract_id_after_segment(uri: &Uri, segment: &str, skip_tokens: &[&str]) -> Option<String> {
let segments: Vec<&str> = uri.path().split('/').filter(|s| !s.is_empty()).collect();
for (i, seg) in segments.iter().enumerate() {
if *seg == segment && i + 1 < segments.len() {
let id = segments[i + 1];
if !skip_tokens.contains(&id) {
return Some(id.to_string());
}
}
}
None
}
pub(crate) async fn resolve_container_role(state: &AppState, uri: &Uri) -> Result<UtilityVmRole> {
let Some(id) = extract_container_id(uri) else {
return Ok(UtilityVmRole::Native);
};
match state.workload_roles.lookup(&id).await {
WorkloadRoleLookup::Found(role) => Ok(role),
WorkloadRoleLookup::Ambiguous => Err(ambiguous_workload_error(&id)),
WorkloadRoleLookup::Missing => match rebuild_container_role_from_guests(state, &id).await {
WorkloadRoleLookup::Found(role) => {
state.workload_roles.record(id.clone(), role).await;
tracing::debug!(
container_id = %id,
utility_vm = role.as_str(),
"rebuilt workload role from guest dockerd",
);
Ok(role)
}
WorkloadRoleLookup::Ambiguous => Err(ambiguous_workload_error(&id)),
WorkloadRoleLookup::Missing => Ok(UtilityVmRole::Native),
},
}
}
pub(crate) async fn resolve_exec_role(state: &AppState, uri: &Uri) -> Result<UtilityVmRole> {
let Some(id) = extract_exec_id(uri) else {
return Ok(UtilityVmRole::Native);
};
match state.workload_roles.lookup(&id).await {
WorkloadRoleLookup::Found(role) => Ok(role),
WorkloadRoleLookup::Ambiguous => Err(ambiguous_workload_error(&id)),
WorkloadRoleLookup::Missing => Ok(UtilityVmRole::Native),
}
}
pub(crate) async fn resolve_role_from_uri(state: &AppState, uri: &Uri) -> Result<UtilityVmRole> {
if let Some(id) = extract_container_id(uri) {
match state.workload_roles.lookup(&id).await {
WorkloadRoleLookup::Found(role) => return Ok(role),
WorkloadRoleLookup::Ambiguous => return Err(ambiguous_workload_error(&id)),
WorkloadRoleLookup::Missing => {}
}
match rebuild_container_role_from_guests(state, &id).await {
WorkloadRoleLookup::Found(role) => {
state.workload_roles.record(id.clone(), role).await;
return Ok(role);
}
WorkloadRoleLookup::Ambiguous => return Err(ambiguous_workload_error(&id)),
WorkloadRoleLookup::Missing => {}
}
}
if let Some(id) = extract_exec_id(uri) {
match state.workload_roles.lookup(&id).await {
WorkloadRoleLookup::Found(role) => return Ok(role),
WorkloadRoleLookup::Ambiguous => return Err(ambiguous_workload_error(&id)),
WorkloadRoleLookup::Missing => {}
}
}
Ok(UtilityVmRole::Native)
}
async fn rebuild_container_role_from_guests(
state: &AppState,
container_id: &str,
) -> WorkloadRoleLookup {
if probe_container_exists(state, UtilityVmRole::Native, container_id).await {
WorkloadRoleLookup::Found(UtilityVmRole::Native)
} else {
WorkloadRoleLookup::Missing
}
}
fn ambiguous_workload_error(id: &str) -> DockerError {
DockerError::Conflict(format!(
"workload identifier '{id}' is ambiguous: it matches multiple workloads. \
Use the full canonical container ID."
))
}
pub(crate) async fn require_amd64_runtime(
state: &AppState,
route: crate::routing::RoutingDecision,
) -> Result<()> {
if crate::routing::is_admissible(route, state.runtime.amd64_runtime_supported()) {
return Ok(());
}
Err(DockerError::NotImplemented(format!(
"linux/amd64 runtime requires FEX in the HV guest, which is not provisioned \
(expected /arcbox/runtime/bin/FEX). amd64 runtime containers are served by FEX \
inside the single HV utility VM; ArcBox does not fall back to a VZ/Rosetta runtime \
VM. Requested platform: {:?}.",
route.platform,
)))
}
async fn probe_container_exists(state: &AppState, role: UtilityVmRole, container_id: &str) -> bool {
use axum::http::{HeaderMap, Method};
use bytes::Bytes;
if ensure_role_ready(state, role).await.is_err() {
return false;
}
let path = format!("/containers/{container_id}/json");
match crate::proxy::proxy_to_guest_for_role(
state.connector.as_ref(),
role,
Method::GET,
&path,
&HeaderMap::new(),
Bytes::new(),
)
.await
{
Ok(resp) => resp.status().is_success(),
Err(_) => false,
}
}
pub(crate) async fn ensure_role_ready(state: &AppState, role: UtilityVmRole) -> Result<()> {
if !state.runtime.role_is_distinct(role) && role != UtilityVmRole::Native {
return Err(DockerError::Server(format!(
"{} utility VM is not available on this host; \
{} workloads require macOS Apple Silicon",
role.as_str(),
role.as_str(),
)));
}
state
.runtime
.ensure_role_ready(role)
.await
.map(|_| ())
.map_err(|e| {
DockerError::Server(format!(
"failed to ensure {} utility VM is ready: {e}",
role.as_str(),
))
})
}
pub(crate) async fn proxy(state: &AppState, uri: &Uri, req: Request<Body>) -> Result<Response> {
proxy_to_role(state, UtilityVmRole::Native, uri, req).await
}
pub(crate) async fn proxy_to_role(
state: &AppState,
role: UtilityVmRole,
uri: &Uri,
req: Request<Body>,
) -> Result<Response> {
ensure_role_ready(state, role).await?;
proxy::proxy_to_guest_stream_for_role(state.connector.as_ref(), role, uri, req).await
}
pub(crate) async fn proxy_upload(
state: &AppState,
uri: &Uri,
req: Request<Body>,
) -> Result<Response> {
proxy_upload_to_role(state, UtilityVmRole::Native, uri, req).await
}
pub(crate) async fn proxy_upload_to_role(
state: &AppState,
role: UtilityVmRole,
uri: &Uri,
req: Request<Body>,
) -> Result<Response> {
ensure_role_ready(state, role).await?;
proxy::proxy_streaming_upload_for_role(state.connector.as_ref(), role, uri, req).await
}
pub(crate) async fn proxy_upgrade_to_role(
state: &AppState,
role: UtilityVmRole,
uri: &Uri,
req: Request<Body>,
) -> Result<Response> {
ensure_role_ready(state, role).await?;
proxy::proxy_with_upgrade_for_role(state.connector.as_ref(), role, req, uri).await
}
mod build;
mod container;
mod events;
mod exec;
mod image;
mod network;
mod system;
mod volume;
pub use build::{build_image, build_prune, session};
pub use container::{
attach_container, container_changes, container_logs, container_stats, container_top,
create_container, extract_container_dns_info, inspect_container, kill_container,
list_containers, pause_container, prune_containers, remove_container, rename_container,
restart_container, start_container, stop_container, unpause_container, wait_container,
};
pub use events::events;
pub use exec::{exec_create, exec_inspect, exec_resize, exec_start};
pub use image::{inspect_image, list_images, load_image, pull_image, remove_image, tag_image};
pub use network::{create_network, inspect_network, list_networks, remove_network};
pub use system::{get_info, get_version, ping};
pub use volume::{create_volume, inspect_volume, list_volumes, prune_volumes, remove_volume};
#[cfg(test)]
mod tests {
use super::*;
fn uri(s: &str) -> Uri {
s.parse().unwrap()
}
#[test]
fn extract_exec_id_from_simple_path() {
assert_eq!(
extract_exec_id(&uri("/exec/exec-abc/start")).as_deref(),
Some("exec-abc"),
);
}
#[test]
fn extract_exec_id_from_versioned_path() {
assert_eq!(
extract_exec_id(&uri("/v1.51/exec/exec-xyz/resize?w=80&h=24")).as_deref(),
Some("exec-xyz"),
);
}
#[test]
fn extract_exec_id_ignores_non_exec_paths() {
assert_eq!(extract_exec_id(&uri("/containers/abc/start")), None);
}
#[test]
fn extract_container_id_from_exec_subpath() {
assert_eq!(
extract_container_id(&uri("/containers/abc/exec")).as_deref(),
Some("abc"),
);
}
}