staxman-old/src/route/stack.rs

171 lines
4.4 KiB
Rust

use crate::{
http::{
error::AppError,
response::{reply, HandlerResponse},
},
node::{
container::ContainerInfo,
stack::{
check_compose, command, commit_compose, create_new, get_compose, get_containers,
write_compose, StackCommand,
},
},
AppState,
};
use askama::Template;
use askama_axum::IntoResponse;
use axum::{
extract::{Path, State},
http::StatusCode,
response::Redirect,
routing::{get, post},
Form, Router,
};
use futures_util::join;
use serde::Deserialize;
use serde_json::json;
#[derive(Template)]
#[template(path = "stack/get-one.html")]
struct GetOneTemplate {
stack_name: String,
file_contents: String,
containers: Vec<ContainerInfo>,
}
#[derive(Template)]
#[template(path = "stack/new-form.html")]
struct CreateTemplate {}
async fn get_one(Path(stack_name): Path<String>, State(state): State<AppState>) -> HandlerResponse {
let (file_contents_res, containers_res) = join!(
get_compose(&state.stack_dir, &stack_name),
get_containers(&state.docker, &stack_name)
);
let file_contents = file_contents_res?;
let containers = containers_res?;
reply(
json!({
"name": stack_name,
"file": file_contents,
"containers": containers,
}),
GetOneTemplate {
stack_name,
file_contents,
containers: containers.iter().map(|c| c.clone().into()).collect(),
},
)
}
async fn new_stack_page() -> impl IntoResponse {
CreateTemplate {}
}
#[derive(Deserialize)]
struct CreateStackForm {
name: String,
source: String,
}
async fn create_stack(
State(state): State<AppState>,
Form(form): Form<CreateStackForm>,
) -> Result<Redirect, AppError> {
// Make sure body is is ok
check_compose(&state.arion_bin, &form.source).await?;
create_new(state.repository, &form.name, &form.source).await?;
Ok(Redirect::to(format!("/stack/{}/", form.name).as_str()))
}
#[derive(Deserialize)]
struct EditStackForm {
source: String,
commit_message: String,
}
async fn edit_stack(
Path(stack_name): Path<String>,
State(state): State<AppState>,
Form(form): Form<EditStackForm>,
) -> Result<Redirect, AppError> {
let commit_message = if form.commit_message.trim().is_empty() {
format!("Update {}", stack_name)
} else {
form.commit_message
};
if form.source.trim().is_empty() {
return Err(AppError::Client {
status: StatusCode::BAD_REQUEST,
code: "invalid-source",
message: "provided stack source is empty".to_string(),
});
};
// Cleanup source (like line endings)
let source = form.source.replace("\r\n", "\n");
// Make sure file is ok
check_compose(&state.arion_bin, &source).await?;
// Write compose file
write_compose(&state.stack_dir, &stack_name, &source).await?;
// Git commit
commit_compose(state.repository, &stack_name, &commit_message)?;
// Update stack
command(
&state.stack_dir,
&stack_name,
&state.arion_bin,
StackCommand::Start,
)
.await?;
Ok(Redirect::to("./"))
}
async fn check_stack_file(
State(state): State<AppState>,
body: String,
) -> Result<StatusCode, AppError> {
check_compose(&state.arion_bin, &body).await?;
Ok(StatusCode::NO_CONTENT)
}
macro_rules! stack_command {
($cmd: expr) => {
move |Path(stack_name): Path<String>, State(state): State<AppState>| async move {
command(&state.stack_dir, &stack_name, &state.arion_bin, $cmd).await?;
Ok(Redirect::to("./")) as Result<Redirect, AppError>
}
};
}
pub(super) fn router() -> Router<AppState> {
Router::new()
.route("/_/new", get(new_stack_page).post(create_stack))
.route("/_/check", post(check_stack_file))
.route(
"/:stack",
get(|Path(stack_name): Path<String>| async move {
Redirect::permanent(format!("{}/", &stack_name).as_str())
}),
)
.route("/:stack/", get(get_one))
.route("/:stack/start", post(stack_command!(StackCommand::Start)))
.route(
"/:stack/restart",
post(stack_command!(StackCommand::Restart)),
)
.route("/:stack/stop", post(stack_command!(StackCommand::Stop)))
.route("/:stack/down", post(stack_command!(StackCommand::Down)))
.route("/:stack/edit", post(edit_stack))
}