copy methods over from old staxman
This commit is contained in:
parent
a168b43fec
commit
915b680070
|
@ -1092,6 +1092,7 @@ dependencies = [
|
|||
"bollard",
|
||||
"clap",
|
||||
"dotenvy",
|
||||
"futures-util",
|
||||
"git2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -20,3 +20,4 @@ tracing = "0.1"
|
|||
tracing-subscriber = "0.3"
|
||||
thiserror = "1"
|
||||
sysinfo = "0.30"
|
||||
futures-util = "0.3"
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
use axum::{
|
||||
async_trait,
|
||||
extract::FromRequestParts,
|
||||
http::{
|
||||
header::{HeaderValue, ACCEPT},
|
||||
request::Parts,
|
||||
StatusCode,
|
||||
},
|
||||
};
|
||||
|
||||
/// Extractor for the Accept header
|
||||
pub struct ExtractAccept(pub HeaderValue);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for ExtractAccept
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
if let Some(accept) = parts.headers.get(ACCEPT) {
|
||||
Ok(ExtractAccept(accept.clone()))
|
||||
} else {
|
||||
Err((StatusCode::NOT_ACCEPTABLE, "`Accept` header is missing"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use axum::{
|
||||
extract::FromRequest,
|
||||
http::{HeaderValue, Request},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_accept() {
|
||||
let req = Request::builder()
|
||||
.header(ACCEPT, "application/json; charset=utf-8")
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let extract = ExtractAccept::from_request(req, &()).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
extract.0,
|
||||
HeaderValue::from_static("application/json; charset=utf-8")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
use axum::{
|
||||
extract::rejection::JsonRejection,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Result type for HTTP requests
|
||||
pub type Result<T> = core::result::Result<T, AppError>;
|
||||
|
||||
/// Error type that can be returned from HTTP requests
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
/// Client error (e.g. bad input, not found)
|
||||
#[error("client error: <{code}> {message}")]
|
||||
Client {
|
||||
status: StatusCode,
|
||||
code: &'static str,
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Server error caused by Docker API errors
|
||||
#[error("docker error: {0}")]
|
||||
DockerError(#[from] bollard::errors::Error),
|
||||
|
||||
/// Server error caused by file operation errors
|
||||
#[error("file error: {0}")]
|
||||
FileError(#[from] std::io::Error),
|
||||
|
||||
/// Server error caused by unexpected internal errors
|
||||
#[error("unexpected internal error: {0}")]
|
||||
Internal(#[from] anyhow::Error),
|
||||
|
||||
/// Errors caused by JSON parsing errors (both incoming and outgoing)
|
||||
#[error("incoming JSON format error: {0}")]
|
||||
JSONFormat(#[from] JsonRejection),
|
||||
}
|
||||
|
||||
pub(super) struct ErrorInfo {
|
||||
pub(super) status: StatusCode,
|
||||
pub(super) code: String,
|
||||
pub(super) message: String,
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let info = match self {
|
||||
AppError::Internal(err) => ErrorInfo {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: "server-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
AppError::FileError(err) => ErrorInfo {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: "file-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
AppError::Client {
|
||||
status,
|
||||
code,
|
||||
message,
|
||||
} => ErrorInfo {
|
||||
status,
|
||||
code: code.to_string(),
|
||||
message: message.to_string(),
|
||||
},
|
||||
AppError::DockerError(err) => match err {
|
||||
bollard::errors::Error::DockerResponseServerError {
|
||||
status_code,
|
||||
message,
|
||||
} => ErrorInfo {
|
||||
status: StatusCode::from_u16(status_code).unwrap(),
|
||||
code: "docker-error".to_string(),
|
||||
message,
|
||||
},
|
||||
_ => ErrorInfo {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: "docker-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
},
|
||||
AppError::JSONFormat(rejection) => {
|
||||
let status = match rejection {
|
||||
JsonRejection::JsonDataError(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
JsonRejection::JsonSyntaxError(_) => StatusCode::BAD_REQUEST,
|
||||
JsonRejection::MissingJsonContentType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
ErrorInfo {
|
||||
status,
|
||||
code: "invalid-body".to_string(),
|
||||
message: rejection.body_text().clone(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
info.status,
|
||||
Json(json!({"code": info.code, "message": info.message})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod accept;
|
||||
pub mod error;
|
|
@ -4,6 +4,7 @@ use clap::{arg, Parser};
|
|||
use std::{net::SocketAddr, path::PathBuf};
|
||||
use tokio::{fs, net::TcpListener};
|
||||
|
||||
mod http;
|
||||
mod node;
|
||||
mod route;
|
||||
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
use crate::http::error::Result;
|
||||
use bollard::{
|
||||
container::{
|
||||
InspectContainerOptions, KillContainerOptions, ListContainersOptions,
|
||||
RemoveContainerOptions, RestartContainerOptions, StartContainerOptions,
|
||||
StopContainerOptions,
|
||||
},
|
||||
service::{ContainerInspectResponse, ContainerSummary, MountPoint},
|
||||
Docker,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContainerInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub state: String,
|
||||
pub image: String,
|
||||
pub image_id: String,
|
||||
pub created_at: String,
|
||||
pub volumes: Option<Vec<MountPoint>>,
|
||||
pub env: Option<Vec<String>>,
|
||||
pub labels: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl ContainerInfo {
|
||||
pub fn stack(&self) -> Option<String> {
|
||||
self.labels
|
||||
.clone()
|
||||
.and_then(|lab| lab.get("com.docker.compose.project").cloned())
|
||||
}
|
||||
|
||||
pub fn running(&self) -> bool {
|
||||
self.state == "running"
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContainerSummary> for ContainerInfo {
|
||||
fn from(value: ContainerSummary) -> Self {
|
||||
let created = OffsetDateTime::from_unix_timestamp(value.created.unwrap())
|
||||
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
ContainerInfo {
|
||||
id: value.id.unwrap_or_default(),
|
||||
name: value.names.unwrap_or_default()[0]
|
||||
.trim_start_matches('/')
|
||||
.to_string(),
|
||||
state: value.state.unwrap_or_default(),
|
||||
image: value.image.unwrap_or_default(),
|
||||
image_id: value.image_id.unwrap_or_default(),
|
||||
created_at: created.time().to_string(),
|
||||
volumes: value.mounts,
|
||||
labels: value.labels,
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContainerInspectResponse> for ContainerInfo {
|
||||
fn from(value: ContainerInspectResponse) -> Self {
|
||||
let config = value.config.unwrap_or_default();
|
||||
ContainerInfo {
|
||||
id: value.id.unwrap_or_default(),
|
||||
name: value.name.unwrap_or_default(),
|
||||
state: value
|
||||
.state
|
||||
.and_then(|s| s.status)
|
||||
.unwrap_or_else(|| bollard::service::ContainerStateStatusEnum::EMPTY)
|
||||
.to_string(),
|
||||
image: config.image.unwrap_or_default(),
|
||||
image_id: value.image.unwrap_or_default(),
|
||||
created_at: value.created.unwrap_or_default(),
|
||||
volumes: value.mounts,
|
||||
labels: config.labels,
|
||||
env: config.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_info(docker: &Docker, name: &str) -> Result<ContainerInfo> {
|
||||
let info = docker
|
||||
.inspect_container(name, Some(InspectContainerOptions { size: true }))
|
||||
.await?;
|
||||
|
||||
Ok(info.into())
|
||||
}
|
||||
|
||||
pub async fn start(docker: &Docker, name: &str) -> Result<()> {
|
||||
Ok(docker
|
||||
.start_container(&name, None::<StartContainerOptions<String>>)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn restart(docker: &Docker, name: &str) -> Result<()> {
|
||||
Ok(docker
|
||||
.restart_container(&name, Some(RestartContainerOptions { t: 30 }))
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn stop(docker: &Docker, name: &str) -> Result<()> {
|
||||
Ok(docker
|
||||
.stop_container(&name, Some(StopContainerOptions { t: 30 }))
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn kill(docker: &Docker, name: &str) -> Result<()> {
|
||||
Ok(docker
|
||||
.kill_container(&name, None::<KillContainerOptions<String>>)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn remove(docker: &Docker, name: &str) -> Result<()> {
|
||||
Ok(docker
|
||||
.remove_container(
|
||||
&name,
|
||||
Some(RemoveContainerOptions {
|
||||
v: false,
|
||||
force: true,
|
||||
link: false,
|
||||
}),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn list(docker: &Docker) -> Result<Vec<ContainerInfo>> {
|
||||
Ok(docker
|
||||
.list_containers(Some(ListContainersOptions::<String> {
|
||||
all: true,
|
||||
limit: None,
|
||||
..Default::default()
|
||||
}))
|
||||
.await?
|
||||
.iter()
|
||||
.map(|c| c.clone().into())
|
||||
.collect())
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod container;
|
||||
pub mod system;
|
||||
|
|
|
@ -35,6 +35,7 @@ pub struct SystemInfo {
|
|||
pub disks: Vec<DiskInfo>,
|
||||
|
||||
// CPU
|
||||
pub cpu_count: usize,
|
||||
pub load_average: (f64, f64, f64),
|
||||
}
|
||||
|
||||
|
@ -54,6 +55,7 @@ pub fn system_info() -> SystemInfo {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let cpus = sys.cpus();
|
||||
let avg = sysinfo::System::load_average();
|
||||
|
||||
let disks = Disks::new_with_refreshed_list()
|
||||
|
@ -72,6 +74,7 @@ pub fn system_info() -> SystemInfo {
|
|||
host_name: sysinfo::System::host_name().unwrap_or_default(),
|
||||
os_version: sysinfo::System::os_version().unwrap_or_default(),
|
||||
kernel_version: sysinfo::System::kernel_version().unwrap_or_default(),
|
||||
cpu_count: cpus.len(),
|
||||
total_memory: sys.total_memory(),
|
||||
used_memory: sys.used_memory(),
|
||||
total_swap: sys.total_swap(),
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
use std::convert::Infallible;
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{
|
||||
sse::{Event, KeepAlive},
|
||||
IntoResponse, Sse,
|
||||
},
|
||||
};
|
||||
use bollard::container::LogsOptions;
|
||||
use futures_util::StreamExt;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{http::accept::ExtractAccept, AppState};
|
||||
|
||||
pub(super) async fn get(
|
||||
Path(container_name): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
ExtractAccept(accept): ExtractAccept,
|
||||
) -> impl IntoResponse {
|
||||
match accept.to_str() {
|
||||
Ok("text/event-stream") => get_log_stream(container_name, state).await.into_response(),
|
||||
_ => get_log_string(container_name, state).await.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_log_string(container_name: String, state: AppState) -> impl IntoResponse {
|
||||
state
|
||||
.docker
|
||||
.logs(
|
||||
&container_name,
|
||||
Some(LogsOptions::<String> {
|
||||
follow: false,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.map(|ev| match ev {
|
||||
Ok(output) => output.to_string(),
|
||||
Err(error) => error.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
async fn get_log_stream(container_name: String, state: AppState) -> impl IntoResponse {
|
||||
let stream = state
|
||||
.docker
|
||||
.logs(
|
||||
&container_name,
|
||||
Some(LogsOptions::<String> {
|
||||
follow: true,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.map(|msg| match msg {
|
||||
Ok(output) => Ok::<_, Infallible>(
|
||||
Event::default()
|
||||
.json_data(json!({ "lines": output.to_string() }))
|
||||
.unwrap(),
|
||||
),
|
||||
Err(error) => Ok(Event::default()
|
||||
.json_data(json!({ "error": error.to_string() }))
|
||||
.unwrap()),
|
||||
});
|
||||
|
||||
Sse::new(stream).keep_alive(KeepAlive::default())
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
use crate::{
|
||||
http::error::Result,
|
||||
node::container::{kill, remove, restart, start, stop},
|
||||
AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::Redirect,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
|
||||
mod log;
|
||||
mod read;
|
||||
|
||||
macro_rules! container_command {
|
||||
($cmd: ident) => {
|
||||
move |Path(cont_name): Path<String>, State(state): State<AppState>| async move {
|
||||
$cmd(&state.docker, &cont_name).await?;
|
||||
Ok(Redirect::to("./")) as Result<Redirect>
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(read::get_all))
|
||||
.route(
|
||||
"/:container",
|
||||
get(|Path(cont_name): Path<String>| async move {
|
||||
Redirect::permanent(format!("{}/", &cont_name).as_str())
|
||||
}),
|
||||
)
|
||||
.route("/:container/", get(read::get_one))
|
||||
.route("/:container/log", get(log::get))
|
||||
.route("/:container/start", post(container_command!(start)))
|
||||
.route("/:container/stop", post(container_command!(stop)))
|
||||
.route("/:container/restart", post(container_command!(restart)))
|
||||
.route("/:container/kill", post(container_command!(kill)))
|
||||
.route(
|
||||
"/:container/remove",
|
||||
post(
|
||||
move |Path(cont_name): Path<String>, State(state): State<AppState>| async move {
|
||||
remove(&state.docker, &cont_name).await?;
|
||||
Ok(Redirect::to("/")) as Result<Redirect>
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
use crate::http::error::Result;
|
||||
use crate::node::container::list;
|
||||
use crate::{node::container::get_info, AppState};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn get_one(
|
||||
Path(container_name): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let info = get_info(&state.docker, &container_name).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"name": container_name,
|
||||
"info": info,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(super) async fn get_all(State(state): State<AppState>) -> Result<impl IntoResponse> {
|
||||
let containers = list(&state.docker).await?;
|
||||
|
||||
Ok(Json(json!(containers)))
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
use crate::{node::system::system_info, AppState};
|
||||
use axum::{response::IntoResponse, routing::get, Json, Router};
|
||||
|
||||
use crate::{node::system::system_info, AppState};
|
||||
mod container;
|
||||
|
||||
async fn get_sys_info() -> impl IntoResponse {
|
||||
Json(system_info())
|
||||
}
|
||||
|
||||
pub(crate) fn router() -> Router<AppState> {
|
||||
Router::new().route("/system-info", get(get_sys_info))
|
||||
Router::new()
|
||||
.route("/system-info", get(get_sys_info))
|
||||
.nest("/containers", container::router())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue