snops 0.1.0

The snarkops control plane responsible for managing environments and agents
use axum::{response::IntoResponse, Json};
use http::StatusCode;
use serde::{ser::SerializeStruct, Serialize, Serializer};
use serde_json::json;
use snops_common::{
    aot_cmds::AotCmdError, db::error::DatabaseError, impl_into_status_code, impl_into_type_str,
};
use thiserror::Error;

use crate::{
    cannon::error::CannonError,
    env::error::{EnvError, EnvRequestError, ExecutionError},
    error::DeserializeError,
    schema::error::{SchemaError, StorageError},
    state::error::BatchReconcileError,
};

#[derive(Debug, Error, strum_macros::AsRefStr)]
pub enum ServerError {
    #[error(transparent)]
    BatchReconcile(#[from] BatchReconcileError),
    #[error("Content resource `{0}` not found")]
    ContentNotFound(String),
    #[error(transparent)]
    Cannon(#[from] CannonError),
    #[error(transparent)]
    Deserialize(#[from] DeserializeError),
    #[error(transparent)]
    Env(#[from] EnvError),
    #[error(transparent)]
    Execute(#[from] ExecutionError),
    #[error(transparent)]
    Schema(#[from] SchemaError),
    #[error(transparent)]
    EnvRequest(#[from] EnvRequestError),
    #[error("{0}")]
    NotFound(String),
    #[error("{0}")]
    BadRequest(String),
    #[error(transparent)]
    AotCmd(#[from] AotCmdError),
    #[error("invalid log level: `{0}`")]
    InvalidLogLevel(String),
    #[error("failed to change log level")]
    FailedToChangeLogLevel,
    #[error(transparent)]
    RpcError(#[from] tarpc::client::RpcError),
    #[error(transparent)]
    Storage(#[from] StorageError),
}

impl_into_status_code!(ServerError, |value| match value {
    BatchReconcile(e) => e.into(),
    ContentNotFound(_) => axum::http::StatusCode::NOT_FOUND,
    Cannon(e) => e.into(),
    Deserialize(e) => e.into(),
    Env(e) => e.into(),
    Execute(e) => e.into(),
    Schema(e) => e.into(),
    EnvRequest(e) => e.into(),
    Storage(e) => e.into(),
    AotCmd(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
    NotFound(_) => axum::http::StatusCode::NOT_FOUND,
    InvalidLogLevel(_) => axum::http::StatusCode::BAD_REQUEST,
    BadRequest(_) => axum::http::StatusCode::BAD_REQUEST,
    FailedToChangeLogLevel => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
    RpcError(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
});

impl_into_type_str!(ServerError, |value| match value {
    BatchReconcile(e) => format!("{}.{e}", value.as_ref()),
    Cannon(e) => format!("{}.{}", value.as_ref(), String::from(e)),
    Env(e) => format!("{}.{}", value.as_ref(), String::from(e)),
    Execute(e) => format!("{}.{}", value.as_ref(), String::from(e)),
    Schema(e) => format!("{}.{}", value.as_ref(), String::from(e)),
    EnvRequest(e) => format!("{}.{}", value.as_ref(), String::from(e)),
    Storage(e) => format!("{}.{}", value.as_ref(), String::from(e)),
    _ => value.as_ref().to_string(),
});

impl Serialize for ServerError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut state = serializer.serialize_struct("Error", 2)?;
        state.serialize_field("type", &String::from(self))?;
        state.serialize_field("error", &self.to_string())?;

        state.end()
    }
}

impl IntoResponse for ServerError {
    fn into_response(self) -> axum::response::Response {
        let json = json!(self);
        let mut res = (StatusCode::from(&self), Json(&json)).into_response();

        res.extensions_mut().insert(json);
        res
    }
}

#[derive(Debug, Error, strum_macros::AsRefStr)]
pub enum StartError {
    #[error("failed to open database: {0}")]
    Database(#[from] DatabaseError),
    #[error("failed to serve: {0}")]
    Serve(#[source] std::io::Error),
    #[error("failed to bind to tcp: {0}")]
    TcpBind(#[source] std::io::Error),
}

#[derive(Debug, Error, Serialize)]
#[serde(untagged)]
pub enum ActionError {
    #[error("execution timed out")]
    ExecuteStatusTimeout {
        tx_id: String,
        agent_id: Option<String>,
        retries: i32,
    },
    #[error("execution aborted")]
    ExecuteStatusAborted { tx_id: String, retries: i32 },
    #[error("execution failed")]
    ExecuteStatusFailed {
        message: String,
        tx_id: String,
        retries: i32,
    },
}

impl_into_status_code!(ActionError, |value| match value {
    ExecuteStatusTimeout { .. } => StatusCode::REQUEST_TIMEOUT,
    ExecuteStatusAborted { .. } | ExecuteStatusFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
});

impl IntoResponse for ActionError {
    fn into_response(self) -> axum::response::Response {
        let mut json = json!(self);
        json["error"] = self.to_string().into();
        (StatusCode::from(&self), Json(&json)).into_response()
    }
}