pulpod 0.0.42

Pulpo daemon — manages agent sessions via tmux/Docker
use std::sync::Arc;

use axum::{Json, http::StatusCode};
use pulpo_common::api::ErrorResponse;
use pulpo_common::peer::PeerInfo;
use serde::de::DeserializeOwned;

use crate::remote::{
    RemoteNodeTarget, apply_remote_auth, normalize_http_base, resolve_peer_target,
};

use super::AppState;

pub(super) type ApiError = (StatusCode, Json<ErrorResponse>);

#[derive(Debug, Clone)]
pub(super) struct RemoteSessionNodeTarget {
    pub session_id: String,
    pub node_name: String,
    pub base_url: String,
    pub token: Option<String>,
}

pub(super) fn internal_error(msg: &str) -> ApiError {
    (
        StatusCode::INTERNAL_SERVER_ERROR,
        Json(ErrorResponse {
            error: msg.to_owned(),
        }),
    )
}

pub(super) fn bad_gateway(msg: &str) -> ApiError {
    (
        StatusCode::BAD_GATEWAY,
        Json(ErrorResponse {
            error: msg.to_owned(),
        }),
    )
}

fn reqwest_status_to_axum(status: reqwest::StatusCode) -> StatusCode {
    StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::BAD_GATEWAY)
}

pub(super) async fn reqwest_error_response(resp: reqwest::Response, fallback: &str) -> ApiError {
    let status = reqwest_status_to_axum(resp.status());
    let error = match resp.json::<ErrorResponse>().await {
        Ok(body) => body.error,
        Err(_) => fallback.to_owned(),
    };
    (status, Json(ErrorResponse { error }))
}

pub(super) async fn send_remote_request(
    request: reqwest::RequestBuilder,
    failure: String,
) -> Result<reqwest::Response, ApiError> {
    request
        .send()
        .await
        .map_err(|e| bad_gateway(&format!("{failure}: {e}")))
}

pub(super) fn remote_json_request(
    target: &RemoteSessionNodeTarget,
    request: reqwest::RequestBuilder,
) -> reqwest::RequestBuilder {
    apply_remote_auth(request, target.token.as_deref())
}

pub(super) async fn parse_remote_json<T: DeserializeOwned>(
    resp: reqwest::Response,
    fallback: &str,
    parse_error: &str,
) -> Result<T, ApiError> {
    if !resp.status().is_success() {
        return Err(reqwest_error_response(resp, fallback).await);
    }

    resp.json::<T>()
        .await
        .map_err(|e| internal_error(&format!("{parse_error}: {e}")))
}

pub(super) async fn expect_remote_no_content(
    resp: reqwest::Response,
    fallback: &str,
) -> Result<StatusCode, ApiError> {
    if !resp.status().is_success() {
        return Err(reqwest_error_response(resp, fallback).await);
    }
    Ok(StatusCode::NO_CONTENT)
}

pub(super) async fn resolve_remote_session_node_target(
    state: &Arc<AppState>,
    id: &str,
) -> Result<Option<RemoteSessionNodeTarget>, ApiError> {
    let Some(session_index) = &state.session_index else {
        return Ok(None);
    };
    let Some(entry) = session_index.get(id).await else {
        return Ok(None);
    };

    let peer: Option<PeerInfo> = state.peer_registry.get(&entry.node_name).await;
    let address = peer
        .as_ref()
        .map(|p| p.address.clone())
        .or_else(|| entry.node_address.clone());

    let Some(address) = address else {
        return Err(bad_gateway(&format!(
            "node address unknown for remote session {id} on node {}",
            entry.node_name
        )));
    };

    let token = state.peer_registry.get_token(&entry.node_name).await;
    Ok(Some(RemoteSessionNodeTarget {
        session_id: entry.session_id,
        node_name: entry.node_name,
        base_url: normalize_http_base(&address),
        token,
    }))
}

pub(super) async fn resolve_remote_node_target(
    state: &Arc<AppState>,
    target_node: &str,
) -> Result<Option<RemoteNodeTarget>, ApiError> {
    let local_name = state.config.read().await.node.name.clone();
    if target_node == local_name {
        return Ok(None);
    }

    let Some(target) = resolve_peer_target(&state.peer_registry, target_node).await else {
        return Err((
            StatusCode::NOT_FOUND,
            Json(ErrorResponse {
                error: format!("target node not found: {target_node}"),
            }),
        ));
    };
    Ok(Some(target))
}