get container status
This commit is contained in:
parent
c961c2ae23
commit
5c5d0b354b
10 changed files with 223 additions and 61 deletions
|
@ -5,6 +5,8 @@ use axum::{
|
|||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T> = core::result::Result<T, AppError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("client error: <{code}> {message}")]
|
||||
|
@ -14,6 +16,12 @@ pub enum AppError {
|
|||
message: &'static str,
|
||||
},
|
||||
|
||||
#[error("docker error: {0}")]
|
||||
DockerError(#[from] bollard::errors::Error),
|
||||
|
||||
#[error("file error: {0}")]
|
||||
FileError(#[from] std::io::Error),
|
||||
|
||||
#[error("unexpected internal error: {0}")]
|
||||
Internal(#[from] anyhow::Error),
|
||||
|
||||
|
@ -25,31 +33,44 @@ pub enum AppError {
|
|||
}
|
||||
|
||||
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 (status, info) = match self {
|
||||
AppError::Internal(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ErrorInfo {
|
||||
code: "server-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
),
|
||||
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::DockerError(err) => ErrorInfo {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: "docker-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
AppError::Template(err) => ErrorInfo {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: "template-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
AppError::Client {
|
||||
status,
|
||||
code,
|
||||
message,
|
||||
} => (
|
||||
} => ErrorInfo {
|
||||
status,
|
||||
ErrorInfo {
|
||||
code: code.to_string(),
|
||||
message: message.to_string(),
|
||||
},
|
||||
),
|
||||
code: code.to_string(),
|
||||
message: message.to_string(),
|
||||
},
|
||||
|
||||
AppError::JSONFormat(rejection) => {
|
||||
let status = match rejection {
|
||||
JsonRejection::JsonDataError(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
|
@ -57,24 +78,15 @@ impl IntoResponse for AppError {
|
|||
JsonRejection::MissingJsonContentType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(
|
||||
status,
|
||||
ErrorInfo {
|
||||
code: "invalid-body".to_string(),
|
||||
message: rejection.body_text().clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
AppError::Template(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ErrorInfo {
|
||||
code: "template-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
),
|
||||
status,
|
||||
code: "invalid-body".to_string(),
|
||||
message: rejection.body_text().clone(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut response = status.into_response();
|
||||
let mut response = info.status.into_response();
|
||||
response.extensions_mut().insert(info);
|
||||
response
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ use serde_json::json;
|
|||
|
||||
use super::error::{AppError, ErrorInfo};
|
||||
|
||||
pub type HandlerResponse = Result<askama_axum::Response, AppError>;
|
||||
|
||||
struct Response {
|
||||
html: String,
|
||||
json: serde_json::Value,
|
||||
|
@ -18,14 +20,11 @@ struct Response {
|
|||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
struct ErrorTemplate {
|
||||
code: String,
|
||||
status: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
pub fn reply<T: Template>(
|
||||
html: T,
|
||||
json: serde_json::Value,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
pub fn reply<T: Template>(json: serde_json::Value, html: T) -> HandlerResponse {
|
||||
let mut response = StatusCode::OK.into_response();
|
||||
response.extensions_mut().insert(Response {
|
||||
html: html.render()?,
|
||||
|
@ -45,12 +44,26 @@ pub async fn response_interceptor<B>(
|
|||
|
||||
let mut response = next.run(request).await;
|
||||
|
||||
if let Some(ErrorInfo { code, message }) = response.extensions_mut().remove::<ErrorInfo>() {
|
||||
if let Some(ErrorInfo {
|
||||
status,
|
||||
code,
|
||||
message,
|
||||
}) = response.extensions_mut().remove::<ErrorInfo>()
|
||||
{
|
||||
match accept_header.as_deref() {
|
||||
Some(b"application/json") => {
|
||||
return Json(json!({"code": code, "message": message})).into_response()
|
||||
return (status, Json(json!({"code": code, "message": message}))).into_response()
|
||||
}
|
||||
_ => {
|
||||
return (
|
||||
status,
|
||||
ErrorTemplate {
|
||||
status: status.to_string(),
|
||||
message,
|
||||
},
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
_ => return ErrorTemplate { code, message }.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
|||
use axum::middleware::from_fn;
|
||||
use bollard::Docker;
|
||||
use clap::Parser;
|
||||
use std::net::SocketAddr;
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
||||
use crate::http::response::response_interceptor;
|
||||
|
||||
|
@ -16,7 +16,7 @@ mod stack;
|
|||
struct Args {
|
||||
/// Path to root of stacks
|
||||
#[arg(short = 'd', long = "stack-dir", env = "STAX_DIR")]
|
||||
stack_dir: String,
|
||||
stack_dir: PathBuf,
|
||||
|
||||
/// Address:port to bind
|
||||
#[arg(short, long, default_value = "0.0.0.0:3000", env = "STAX_BIND")]
|
||||
|
@ -25,7 +25,7 @@ struct Args {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub stack_dir: String,
|
||||
pub stack_dir: PathBuf,
|
||||
pub docker: Docker,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
use askama::Template;
|
||||
use askama_axum::IntoResponse;
|
||||
use axum::{extract::State, routing::get, Router};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
http::{error::AppError, response::reply},
|
||||
stack, AppState,
|
||||
http::{
|
||||
error::AppError,
|
||||
response::{reply, HandlerResponse},
|
||||
},
|
||||
AppState,
|
||||
};
|
||||
|
||||
mod stack;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "home.html")]
|
||||
struct HomeTemplate {
|
||||
stacks: Vec<String>,
|
||||
}
|
||||
|
||||
async fn home(State(state): State<AppState>) -> Result<impl IntoResponse, AppError> {
|
||||
let list = stack::list(&state.stack_dir)
|
||||
async fn home(State(state): State<AppState>) -> HandlerResponse {
|
||||
let list = crate::stack::list(&state.stack_dir)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
reply(
|
||||
HomeTemplate {
|
||||
stacks: list.clone(),
|
||||
},
|
||||
json!({ "stacks": list}),
|
||||
)
|
||||
reply(json!({ "stacks": list }), HomeTemplate { stacks: list })
|
||||
}
|
||||
|
||||
pub(super) fn router() -> Router<AppState> {
|
||||
Router::new().route("/", get(home))
|
||||
pub(crate) fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(home))
|
||||
.nest("/stack", stack::router())
|
||||
}
|
||||
|
|
60
src/route/stack.rs
Normal file
60
src/route/stack.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use bollard::service::ContainerSummary;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
http::response::{reply, HandlerResponse},
|
||||
stack::{get_compose, get_containers},
|
||||
AppState,
|
||||
};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "stack/get-one.html")]
|
||||
struct GetOneTemplate {
|
||||
stack_name: String,
|
||||
file_contents: String,
|
||||
containers: Vec<ContainerInfo>,
|
||||
}
|
||||
|
||||
struct ContainerInfo {
|
||||
name: String,
|
||||
state: String,
|
||||
image: String,
|
||||
}
|
||||
|
||||
impl From<ContainerSummary> for ContainerInfo {
|
||||
fn from(value: ContainerSummary) -> Self {
|
||||
ContainerInfo {
|
||||
name: value.names.unwrap().join(","),
|
||||
state: value.state.unwrap(),
|
||||
image: value.image.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_one(Path(stack_name): Path<String>, State(state): State<AppState>) -> HandlerResponse {
|
||||
let file_contents = get_compose(&state.stack_dir, &stack_name).await?;
|
||||
let containers = get_containers(&state.docker, &stack_name).await?;
|
||||
|
||||
reply(
|
||||
json!({
|
||||
"name": stack_name,
|
||||
"file": file_contents,
|
||||
"containers": containers,
|
||||
}),
|
||||
GetOneTemplate {
|
||||
stack_name,
|
||||
file_contents,
|
||||
containers: containers.iter().map(|c| c.clone().into()).collect(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn router() -> Router<AppState> {
|
||||
Router::new().route("/:stack", get(get_one))
|
||||
}
|
22
src/stack/error.rs
Normal file
22
src/stack/error.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use axum::http::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::http::error::AppError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum StackError {
|
||||
#[error("stack not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl Into<AppError> for StackError {
|
||||
fn into(self) -> AppError {
|
||||
match self {
|
||||
StackError::NotFound => AppError::Client {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
code: "stack-not-found",
|
||||
message: "stack not found",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,44 @@
|
|||
use anyhow::Result;
|
||||
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use tokio::fs::{read_dir, try_exists};
|
||||
|
||||
pub async fn list(base_dir: &str) -> Result<Vec<String>> {
|
||||
use crate::http::error::Result;
|
||||
|
||||
use self::error::StackError;
|
||||
|
||||
pub mod error;
|
||||
|
||||
const COMPOSE_FILE: &str = "arion-compose.nix";
|
||||
|
||||
async fn is_stack(dir: &PathBuf) -> Result<bool> {
|
||||
Ok(try_exists(dir.join(COMPOSE_FILE)).await?)
|
||||
}
|
||||
|
||||
pub async fn get_containers(docker: &Docker, stack_name: &str) -> Result<Vec<ContainerSummary>> {
|
||||
Ok(docker
|
||||
.list_containers(Some(ListContainersOptions {
|
||||
all: true,
|
||||
limit: None,
|
||||
size: true,
|
||||
filters: HashMap::from([(
|
||||
"label".to_string(),
|
||||
vec![format!("com.docker.compose.project={}", stack_name)],
|
||||
)]),
|
||||
}))
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_compose(base_dir: &PathBuf, stack_name: &str) -> Result<String> {
|
||||
let dir = base_dir.join(stack_name);
|
||||
if !is_stack(&dir).await? {
|
||||
return Err(StackError::NotFound.into());
|
||||
}
|
||||
|
||||
let contents = tokio::fs::read_to_string(dir.join(COMPOSE_FILE)).await?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
pub async fn list(base_dir: &PathBuf) -> Result<Vec<String>> {
|
||||
let mut dirs = read_dir(base_dir).await?;
|
||||
let mut stacklist = vec![];
|
||||
while let Some(dir) = dirs.next_entry().await? {
|
||||
|
@ -9,7 +46,7 @@ pub async fn list(base_dir: &str) -> Result<Vec<String>> {
|
|||
if !meta.is_dir() {
|
||||
continue;
|
||||
}
|
||||
if try_exists(dir.path().join("arion-compose.nix")).await? {
|
||||
if is_stack(&dir.path()).await? {
|
||||
stacklist.push(dir.file_name().to_string_lossy().to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block content %}
|
||||
<main class="error-page">
|
||||
<h1>{{ code }}</h1>
|
||||
<h1>{{ status }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
{% block content %}
|
||||
<main>
|
||||
{% for stack in stacks %}
|
||||
<li><a href="/stack/{{stack}}">{{stack}}</a></li>
|
||||
{% endfor %}
|
||||
<ul>
|
||||
{% for stack in stacks %}
|
||||
<li><a href="/stack/{{stack}}">{{stack}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</main>
|
||||
{% endblock %}
|
15
templates/stack/get-one.html
Normal file
15
templates/stack/get-one.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Viewing {{stack_name}}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<h1>{{stack_name}}</h1>
|
||||
<textarea>{{file_contents}}</textarea>
|
||||
<ul>
|
||||
{% for container in containers %}
|
||||
<li>{{container.name}} ({{container.image}}) : {{ container.state }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</main>
|
||||
{% endblock %}
|
Reference in a new issue