use std::io;
use std::net::SocketAddr;
use axum::Json;
use axum::extract::FromRequest;
use axum::extract::FromRequestParts;
use axum::extract::Query;
use axum::extract::rejection::{JsonRejection, QueryRejection};
use axum::http::StatusCode;
use axum::http::request::Parts;
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::ids::IdError;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(
clippy::error_impl_error,
reason = "Error is the crate's public error type"
)]
pub enum Error {
#[error("at least one API key is required")]
NoApiKeys,
#[error("API key must be id:secret")]
ApiKeyMissingSeparator,
#[error("API key file {path}: line {line}: expected id=secret")]
ApiKeyFileSyntax {
path: String,
line: usize,
},
#[error("API key {0:?} has an empty secret")]
EmptyApiKeySecret(String),
#[error("duplicate API key id {0:?}")]
DuplicateApiKeyId(String),
#[error(transparent)]
InvalidId(#[from] IdError),
#[error("invalid listen address {0:?}")]
InvalidListen(String),
#[error("refusing non-loopback listen {0}")]
NonLoopback(SocketAddr),
#[error("--public requires a TCP listen address")]
PublicRequiresTcp,
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Runtime(#[from] bux::Error),
}
impl Error {
#[must_use]
pub const fn exit_code(&self) -> i32 {
match self {
Self::Io(_) | Self::Runtime(_) => 1,
_ => 2,
}
}
}
#[derive(Debug)]
pub(crate) struct ApiError {
status: StatusCode,
code: &'static str,
message: String,
existing_id: Option<String>,
field: Option<String>,
}
impl ApiError {
pub(crate) fn unauthorized() -> Self {
Self {
status: StatusCode::UNAUTHORIZED,
code: "unauthorized",
message: "missing or invalid bearer token".into(),
existing_id: None,
field: None,
}
}
pub(crate) fn payload_too_large() -> Self {
Self::payload_too_large_msg("request body too large")
}
pub(crate) fn payload_too_large_msg(message: impl Into<String>) -> Self {
Self {
status: StatusCode::PAYLOAD_TOO_LARGE,
code: "payload_too_large",
message: message.into(),
existing_id: None,
field: None,
}
}
pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
code: "invalid_config",
message: message.into(),
existing_id: None,
field: None,
}
}
pub(crate) fn not_found() -> Self {
Self::not_found_msg("sandbox not found")
}
pub(crate) fn not_found_msg(message: impl Into<String>) -> Self {
Self {
status: StatusCode::NOT_FOUND,
code: "not_found",
message: message.into(),
existing_id: None,
field: None,
}
}
pub(crate) fn image_in_use() -> Self {
Self {
status: StatusCode::CONFLICT,
code: "busy",
message: "image is in use".into(),
existing_id: None,
field: None,
}
}
pub(crate) fn oci(message: impl Into<String>) -> Self {
Self {
status: StatusCode::BAD_GATEWAY,
code: "oci",
message: message.into(),
existing_id: None,
field: None,
}
}
pub(crate) fn name_occupied(existing_id: impl Into<String>) -> Self {
Self {
status: StatusCode::CONFLICT,
code: "name_occupied",
message: "sandbox name is occupied".into(),
existing_id: Some(existing_id.into()),
field: None,
}
}
pub(crate) fn name_occupied_unknown() -> Self {
Self {
status: StatusCode::CONFLICT,
code: "name_occupied",
message: "sandbox name is occupied".into(),
existing_id: None,
field: None,
}
}
pub(crate) fn sandbox_exists(existing_id: impl Into<String>, field: &'static str) -> Self {
Self {
status: StatusCode::CONFLICT,
code: "sandbox_exists",
message: "sandbox exists with a different spec".into(),
existing_id: Some(existing_id.into()),
field: Some(field.into()),
}
}
pub(crate) fn still_stopping() -> Self {
Self {
status: StatusCode::SERVICE_UNAVAILABLE,
code: "guest_unavailable",
message: "sandbox still stopping".into(),
existing_id: None,
field: None,
}
}
pub(crate) fn resource_exhausted(message: impl Into<String>) -> Self {
Self {
status: StatusCode::TOO_MANY_REQUESTS,
code: "resource_exhausted",
message: message.into(),
existing_id: None,
field: None,
}
}
pub(crate) fn internal(message: impl Into<String>) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: "internal",
message: message.into(),
existing_id: None,
field: None,
}
}
pub(crate) fn with_field(mut self, field: &'static str) -> Self {
self.field = Some(field.into());
self
}
pub(crate) fn from_engine(err: bux::Error) -> Self {
match err {
bux::Error::InvalidConfig(message) => Self::invalid_config(message),
bux::Error::NotFound(_) => Self::not_found(),
bux::Error::Ambiguous(_) => Self::name_occupied_unknown(),
bux::Error::InvalidState(message) => Self::conflict("invalid_state", message),
bux::Error::Busy(message) => Self::conflict("busy", message),
bux::Error::GuestUnavailable(message) => Self {
status: StatusCode::SERVICE_UNAVAILABLE,
code: "guest_unavailable",
message,
existing_id: None,
field: None,
},
bux::Error::SecretsRequired => {
Self::conflict("secrets_required", "secrets required for this sandbox")
}
bux::Error::SecretsNeedVirtioNet => Self::invalid_config("secrets require virtio-net"),
bux::Error::SecurityUnavailable(message) => Self {
status: StatusCode::PRECONDITION_FAILED,
code: "security_unavailable",
message,
existing_id: None,
field: None,
},
bux::Error::Oci(e) => map_oci(e),
bux::Error::Shutdown => Self {
status: StatusCode::SERVICE_UNAVAILABLE,
code: "shutdown",
message: "runtime has been shut down".into(),
existing_id: None,
field: None,
},
other => Self::internal(other.to_string()),
}
}
fn conflict(code: &'static str, message: impl Into<String>) -> Self {
Self {
status: StatusCode::CONFLICT,
code,
message: message.into(),
existing_id: None,
field: None,
}
}
}
fn map_oci(err: impl std::fmt::Display) -> ApiError {
let message = err.to_string();
if message.starts_with("invalid image reference") {
ApiError::invalid_config(message)
} else if message.starts_with("image not found") {
ApiError::not_found_msg(message)
} else {
ApiError::oci(message)
}
}
impl From<IdError> for ApiError {
fn from(err: IdError) -> Self {
Self::invalid_config(err.to_string())
}
}
pub(crate) struct JsonBody<T>(pub T);
impl<S, T> FromRequest<S> for JsonBody<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = ApiError;
async fn from_request(
req: axum::extract::Request,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
match Json::<T>::from_request(req, state).await {
Ok(Json(value)) => Ok(Self(value)),
Err(rejection) => Err(json_rejection(&rejection)),
}
}
}
fn json_rejection(rejection: &JsonRejection) -> ApiError {
ApiError::invalid_config(rejection.body_text())
}
pub(crate) struct QueryBody<T>(pub T);
impl<S, T> FromRequestParts<S> for QueryBody<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = ApiError;
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
match Query::<T>::from_request_parts(parts, state).await {
Ok(Query(value)) => Ok(Self(value)),
Err(rejection) => Err(query_rejection(&rejection)),
}
}
}
fn query_rejection(rejection: &QueryRejection) -> ApiError {
ApiError::invalid_config(rejection.body_text())
}
#[derive(Serialize)]
struct ErrorEnvelope<'a> {
error: ErrorBody<'a>,
}
#[derive(Serialize)]
struct ErrorBody<'a> {
code: &'a str,
message: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
existing_id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
field: Option<&'a str>,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let body = ErrorEnvelope {
error: ErrorBody {
code: self.code,
message: &self.message,
existing_id: self.existing_id.as_deref(),
field: self.field.as_deref(),
},
};
(self.status, Json(body)).into_response()
}
}