arcbox-docker 0.4.9

Docker REST API compatibility layer for ArcBox
use super::{proxy_to_role, proxy_upgrade_to_role, resolve_container_role, resolve_exec_role};
use crate::api::AppState;
use crate::error::{DockerError, Result};
use axum::body::Body;
use axum::extract::{OriginalUri, State};
use axum::http::{Request, header};
use axum::response::Response;

crate::handlers::exec_proxy_handler!(exec_resize);
crate::handlers::exec_proxy_handler!(exec_inspect);

/// Create an exec instance on a container, recording the resulting exec ID
/// against the container's utility VM role so follow-up
/// `start`/`resize`/`inspect` calls land on the same VM.
///
/// # Errors
///
/// Returns an error if VM readiness fails or proxying to guest dockerd fails.
pub async fn exec_create(
    State(state): State<AppState>,
    OriginalUri(uri): OriginalUri,
    req: Request<Body>,
) -> Result<Response> {
    let role = resolve_container_role(&state, &uri).await?;
    let response = proxy_to_role(&state, role, &uri, req).await?;
    if !response.status().is_success() {
        return Ok(response);
    }

    // Buffer the create response so the exec ID can be recorded. Exec create
    // responses are small JSON (just `Id`).
    let (parts, body) = response.into_parts();
    let body_bytes = http_body_util::BodyExt::collect(body)
        .await
        .map_err(|e| DockerError::Server(format!("failed to read exec create response: {e}")))?
        .to_bytes();

    if let Some(exec_id) = parse_exec_create_response_id(&body_bytes) {
        tracing::debug!(
            utility_vm = role.as_str(),
            exec_id = %exec_id,
            "recorded exec role binding",
        );
        state.workload_roles.record(exec_id, role).await;
    } else {
        tracing::warn!(
            utility_vm = role.as_str(),
            "exec create response missing exec ID; follow-up calls will fall back to native"
        );
    }

    Ok(Response::from_parts(parts, Body::from(body_bytes)))
}

/// Start exec instance (proxy + upgrade for interactive mode).
///
/// # Errors
///
/// Returns an error if upgrade proxying or request proxying fails.
pub async fn exec_start(
    State(state): State<AppState>,
    OriginalUri(uri): OriginalUri,
    req: Request<Body>,
) -> Result<Response> {
    let role = resolve_exec_role(&state, &uri).await?;
    let wants_upgrade = req.headers().get(header::UPGRADE).is_some()
        || req
            .headers()
            .get(header::CONNECTION)
            .and_then(|v| v.to_str().ok())
            .is_some_and(|v| v.to_ascii_lowercase().contains("upgrade"));

    if wants_upgrade {
        proxy_upgrade_to_role(&state, role, &uri, req).await
    } else {
        proxy_to_role(&state, role, &uri, req).await
    }
}

/// Parses the `Id` field from a `POST /containers/{id}/exec` JSON response.
fn parse_exec_create_response_id(body: &[u8]) -> Option<String> {
    let value: serde_json::Value = serde_json::from_slice(body).ok()?;
    value.get("Id")?.as_str().map(String::from)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_exec_id_from_create_response() {
        let json = br#"{"Id":"exec-abc123"}"#;
        assert_eq!(
            parse_exec_create_response_id(json).as_deref(),
            Some("exec-abc123"),
        );
    }

    #[test]
    fn returns_none_when_exec_create_response_missing_id() {
        assert_eq!(parse_exec_create_response_id(b"{}"), None);
        assert_eq!(parse_exec_create_response_id(b"not json"), None);
    }
}