use super::{extract_container_id, proxy_to_system_vm, require_amd64_runtime};
use crate::api::AppState;
use crate::error::{DockerError, Result};
use crate::port_bindings::parse_port_bindings;
use crate::routing::{query_param, route_container_create};
use axum::body::Body;
use axum::extract::{OriginalUri, State};
use axum::http::{Request, Uri};
use axum::response::Response;
use bytes::Bytes;
use std::net::IpAddr;
#[tracing::instrument(
name = "docker.container.create",
skip(state, req),
fields(
uri = %uri,
utility_vm = "native",
translator = tracing::field::Empty,
container_id = tracing::field::Empty,
name = tracing::field::Empty,
),
err
)]
pub async fn create_container(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
let (parts, body) = req.into_parts();
let body_bytes = http_body_util::BodyExt::collect(body)
.await
.map_err(|e| DockerError::Server(format!("failed to read body: {e}")))?
.to_bytes();
let body_bytes = crate::host_path::rewrite_create_body(body_bytes);
let route = route_container_create(&uri, &body_bytes);
let requested_name = query_param(&uri, "name").map(str::to_string);
tracing::Span::current().record("translator", route.translator.as_str());
if let Some(name) = requested_name.as_deref() {
tracing::Span::current().record("name", name);
}
require_amd64_runtime(&state, route).await?;
tracing::debug!(
backend = "hv",
translator = route.translator.as_str(),
platform = ?route.platform,
name = requested_name.as_deref().unwrap_or(""),
"routing Docker container create request"
);
let mut req = Request::from_parts(parts, Body::from(body_bytes));
req.headers_mut().remove(axum::http::header::CONTENT_LENGTH);
proxy_to_system_vm(&state, &uri, req).await
}
#[tracing::instrument(
name = "docker.container.start",
skip(state, req),
fields(uri = %uri, utility_vm = "native", container_id = tracing::field::Empty),
err
)]
pub async fn start_container(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
let container_id = extract_container_id(&uri);
if let Some(id) = container_id.as_deref() {
tracing::Span::current().record("container_id", id);
}
let response = proxy_to_system_vm(&state, &uri, req).await?;
if response.status().is_success() {
if let Some(ref id) = container_id {
setup_container_networking(&state, id).await;
}
}
Ok(response)
}
#[tracing::instrument(
name = "docker.container.stop",
skip(state, req),
fields(uri = %uri, utility_vm = "native", container_id = tracing::field::Empty),
err
)]
pub async fn stop_container(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
if let Some(id) = extract_container_id(&uri) {
tracing::Span::current().record("container_id", id.as_str());
}
let canonical = resolve_or_raw_for_teardown(&state, &uri).await;
let response = proxy_to_system_vm(&state, &uri, req).await?;
let status = response.status().as_u16();
if status == 204 || status == 304 {
if let Some(canonical) = canonical {
state.runtime.stop_port_forwarding_by_id(&canonical).await;
state.runtime.deregister_dns_by_id(&canonical).await;
}
}
Ok(response)
}
fn kill_terminates_container(uri: &Uri) -> bool {
match query_param(uri, "signal") {
None => true, Some(signal) => {
let signal = signal.trim();
signal.eq_ignore_ascii_case("SIGKILL")
|| signal.eq_ignore_ascii_case("KILL")
|| signal == "9"
}
}
}
#[tracing::instrument(
name = "docker.container.kill",
skip(state, req),
fields(uri = %uri, utility_vm = "native", container_id = tracing::field::Empty),
err
)]
pub async fn kill_container(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
if let Some(id) = extract_container_id(&uri) {
tracing::Span::current().record("container_id", id.as_str());
}
let canonical = resolve_or_raw_for_teardown(&state, &uri).await;
let terminates = kill_terminates_container(&uri);
let response = proxy_to_system_vm(&state, &uri, req).await?;
if response.status().as_u16() == 204 && terminates {
if let Some(canonical) = canonical {
state.runtime.stop_port_forwarding_by_id(&canonical).await;
state.runtime.deregister_dns_by_id(&canonical).await;
}
}
Ok(response)
}
#[tracing::instrument(
name = "docker.container.restart",
skip(state, req),
fields(uri = %uri, utility_vm = "native", container_id = tracing::field::Empty),
err
)]
pub async fn restart_container(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
if let Some(id) = extract_container_id(&uri) {
tracing::Span::current().record("container_id", id.as_str());
}
let response = proxy_to_system_vm(&state, &uri, req).await?;
if response.status().as_u16() == 204 {
if let Some(id) = extract_container_id(&uri) {
let _ = state.runtime.ensure_vm_ready().await;
if let Some(body_bytes) = inspect_container_body(&state, &id).await {
let canonical = canonical_id_or_fallback(&id, &body_bytes);
if let Some(name) = extract_container_name(&body_bytes) {
state
.runtime
.register_container_alias(&name, &canonical)
.await;
}
if let Some((aliases, ip)) = extract_container_dns_info(&body_bytes) {
state.runtime.register_dns(&canonical, &aliases, ip).await;
}
}
}
}
Ok(response)
}
#[tracing::instrument(
name = "docker.container.remove",
skip(state, req),
fields(uri = %uri, utility_vm = "native", container_id = tracing::field::Empty),
err
)]
pub async fn remove_container(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
if let Some(id) = extract_container_id(&uri) {
tracing::Span::current().record("container_id", id.as_str());
}
let canonical = resolve_or_raw_for_teardown(&state, &uri).await;
let response = proxy_to_system_vm(&state, &uri, req).await?;
if response.status().is_success() {
if let Some(canonical) = canonical {
state.runtime.stop_port_forwarding_by_id(&canonical).await;
state.runtime.deregister_dns_by_id(&canonical).await;
}
}
Ok(response)
}
async fn setup_container_networking(state: &AppState, container_id: &str) {
let Some(body_bytes) = inspect_container_body(state, container_id).await else {
tracing::warn!(
container_id,
"Failed to inspect container for networking setup; \
port forwarding and DNS will not be configured"
);
return;
};
let canonical_id = canonical_id_or_fallback(container_id, &body_bytes);
if let Some(name) = extract_container_name(&body_bytes) {
state
.runtime
.register_container_alias(&name, &canonical_id)
.await;
}
setup_port_forwarding_from_inspect(state, &canonical_id, &body_bytes).await;
if let Some((aliases, ip)) = extract_container_dns_info(&body_bytes) {
state
.runtime
.register_dns(&canonical_id, &aliases, ip)
.await;
}
}
async fn inspect_container_body(state: &AppState, container_id: &str) -> Option<Bytes> {
crate::guest_query::inspect_container(state.proxy.client(), container_id).await
}
async fn setup_port_forwarding_from_inspect(
state: &AppState,
canonical_id: &str,
body_bytes: &[u8],
) {
let bindings = parse_port_bindings(body_bytes);
if bindings.is_empty() {
tracing::debug!("No port bindings found for container {}", canonical_id);
return;
}
tracing::info!(
"Port forwarding: {} bindings for container {}",
bindings.len(),
canonical_id,
);
for b in &bindings {
tracing::info!(
" bind {}:{} → container:{}/{}",
b.host_ip,
b.host_port,
b.container_port,
b.protocol,
);
}
let rules: Vec<_> = bindings
.iter()
.map(|b| {
(
b.host_ip.clone(),
b.host_port,
b.container_port,
b.protocol.clone(),
)
})
.collect();
let machine_name = state.runtime.default_machine_name();
if let Err(e) = state
.runtime
.start_port_forwarding_for(machine_name, canonical_id, &rules)
.await
{
tracing::warn!(
utility_vm = "native",
"Failed to start port forwarding for {}: {}",
canonical_id,
e,
);
}
}
pub fn extract_container_dns_info(inspect_json: &[u8]) -> Option<(Vec<String>, IpAddr)> {
let v: serde_json::Value = serde_json::from_slice(inspect_json).ok()?;
let name = v.get("Name")?.as_str()?.trim_start_matches('/').to_string();
if name.is_empty() {
return None;
}
let ip_str = v
.pointer("/NetworkSettings/IPAddress")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.or_else(|| {
v.pointer("/NetworkSettings/Networks")?
.as_object()?
.values()
.find_map(|net| net.get("IPAddress")?.as_str().filter(|s| !s.is_empty()))
})?;
let aliases = match v.pointer("/Config/Labels").and_then(|l| l.as_object()) {
Some(labels) => {
let project = labels
.get("com.docker.compose.project")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
let service = labels
.get("com.docker.compose.service")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
match (project, service) {
(Some(proj), Some(svc)) => {
vec![format!("{svc}.{proj}"), name]
}
_ => vec![name],
}
}
None => vec![name],
};
Some((aliases, ip_str.parse().ok()?))
}
#[tracing::instrument(
name = "docker.container.rename",
skip(state, req),
fields(
uri = %uri,
utility_vm = "native",
container_id = tracing::field::Empty,
new_name = tracing::field::Empty,
),
err
)]
pub async fn rename_container(
State(state): State<AppState>,
OriginalUri(uri): OriginalUri,
req: Request<Body>,
) -> Result<Response> {
if let Some(id) = extract_container_id(&uri) {
tracing::Span::current().record("container_id", id.as_str());
}
let canonical = resolve_canonical_from_uri(&state, &uri).await;
let new_name = query_param(&uri, "name").map(str::to_string);
if let Some(name) = new_name.as_deref() {
tracing::Span::current().record("new_name", name);
}
let response = proxy_to_system_vm(&state, &uri, req).await?;
if response.status().is_success() {
if let Some(ref canonical) = canonical {
let refreshed = match inspect_container_body(&state, canonical).await {
Some(body_bytes) => {
let name = extract_container_name(&body_bytes);
let dns = extract_container_dns_info(&body_bytes);
if name.is_none() && dns.is_none() {
false
} else {
state.runtime.deregister_dns_by_id(canonical).await;
if let Some(name) = name.as_deref() {
state
.runtime
.register_container_alias(name, canonical)
.await;
}
if let Some((aliases, ip)) = dns {
state.runtime.register_dns(canonical, &aliases, ip).await;
}
true
}
}
None => false,
};
if !refreshed {
if let Some(name) = new_name.as_deref() {
state
.runtime
.register_container_alias(name, canonical)
.await;
}
tracing::warn!(
container_id = %canonical,
"post-rename inspect unusable; keeping previous DNS registration"
);
}
}
}
Ok(response)
}
fn extract_canonical_id_from_inspect(inspect_json: &[u8]) -> Option<String> {
let value: serde_json::Value = serde_json::from_slice(inspect_json).ok()?;
value.get("Id")?.as_str().map(String::from)
}
pub(super) fn canonical_id_or_fallback(container_id: &str, inspect_json: &[u8]) -> String {
extract_canonical_id_from_inspect(inspect_json).unwrap_or_else(|| container_id.to_string())
}
pub fn extract_container_name(inspect_json: &[u8]) -> Option<String> {
let value: serde_json::Value = serde_json::from_slice(inspect_json).ok()?;
let name = value.get("Name")?.as_str()?.trim_start_matches('/');
(!name.is_empty()).then(|| name.to_string())
}
async fn resolve_canonical_from_uri(state: &AppState, uri: &Uri) -> Option<String> {
let token = extract_container_id(uri)?;
state.runtime.resolve_registered_container(&token).await
}
async fn resolve_or_raw_for_teardown(state: &AppState, uri: &Uri) -> Option<String> {
let token = extract_container_id(uri)?;
Some(
state
.runtime
.resolve_registered_container(&token)
.await
.unwrap_or(token),
)
}
#[cfg(test)]
mod tests;