reorganize code
This commit is contained in:
parent
e3fecbee95
commit
d7dd7ca7be
16 changed files with 450 additions and 373 deletions
|
@ -7,20 +7,23 @@ pub struct StackComposeInfo {
|
|||
pub services: Vec<String>,
|
||||
}
|
||||
|
||||
const PROJECT_KEY: &str = "project\0name";
|
||||
const SERVICES_PREFIX: &str = "services\0";
|
||||
|
||||
pub fn parse_arion_compose(file: &str) -> Result<StackComposeInfo> {
|
||||
let ast = rnix::Root::parse(file).ok()?;
|
||||
let expr = ast.expr().ok_or_else(|| anyhow!("invalid nix root"))?;
|
||||
let flattened = flatten_set(expr)?;
|
||||
|
||||
let project = flattened["project\0name"]
|
||||
.clone()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
if !flattened.contains_key(PROJECT_KEY) {
|
||||
return Err(anyhow!("missing required key 'project.name'"));
|
||||
}
|
||||
let project = flattened[PROJECT_KEY].clone().trim_matches('"').to_string();
|
||||
|
||||
let services = flattened
|
||||
.iter()
|
||||
.filter_map(|(key, _)| {
|
||||
key.strip_prefix("services\0")
|
||||
key.strip_prefix(SERVICES_PREFIX)
|
||||
.and_then(|key| key.split_once('\0'))
|
||||
.and_then(|k| Some(k.0.to_string()))
|
||||
})
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
use crate::{
|
||||
http::{
|
||||
accept::ExtractAccept,
|
||||
response::{reply, HandlerResponse},
|
||||
},
|
||||
node::container::{get_info, kill, remove, restart, start, stop, ContainerInfo},
|
||||
route::AppError,
|
||||
AppState,
|
||||
};
|
||||
use askama::Template;
|
||||
use askama_axum::IntoResponse;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{
|
||||
sse::{Event, KeepAlive},
|
||||
Redirect, Sse,
|
||||
},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use bollard::container::LogsOptions;
|
||||
use futures_util::stream::StreamExt;
|
||||
use serde_json::json;
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "container/get-one.html")]
|
||||
struct GetOneTemplate {
|
||||
container_name: String,
|
||||
info: ContainerInfo,
|
||||
}
|
||||
|
||||
async fn get_one(
|
||||
Path(container_name): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> HandlerResponse {
|
||||
let info = get_info(&state.docker, &container_name).await?;
|
||||
|
||||
reply(
|
||||
json!({
|
||||
"name": container_name,
|
||||
"info": info,
|
||||
}),
|
||||
GetOneTemplate {
|
||||
container_name,
|
||||
info,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_log(
|
||||
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())
|
||||
}
|
||||
|
||||
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, AppError>
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/:container",
|
||||
get(|Path(cont_name): Path<String>| async move {
|
||||
Redirect::permanent(format!("{}/", &cont_name).as_str())
|
||||
}),
|
||||
)
|
||||
.route("/:container/", get(get_one))
|
||||
.route("/:container/log", get(get_log))
|
||||
.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, AppError>
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
73
src/route/container/log.rs
Normal file
73
src/route/container/log.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use std::convert::Infallible;
|
||||
|
||||
use askama_axum::IntoResponse;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::sse::{Event, KeepAlive},
|
||||
response::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())
|
||||
}
|
48
src/route/container/mod.rs
Normal file
48
src/route/container/mod.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use crate::{
|
||||
node::container::{kill, remove, restart, start, stop},
|
||||
route::AppError,
|
||||
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, AppError>
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.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, AppError>
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
34
src/route/container/read.rs
Normal file
34
src/route/container/read.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use askama::Template;
|
||||
use axum::extract::{Path, State};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
http::response::{reply, HandlerResponse},
|
||||
node::container::{get_info, ContainerInfo},
|
||||
AppState,
|
||||
};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "container/get-one.html")]
|
||||
struct GetOneTemplate {
|
||||
container_name: String,
|
||||
info: ContainerInfo,
|
||||
}
|
||||
|
||||
pub(super) async fn get_one(
|
||||
Path(container_name): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> HandlerResponse {
|
||||
let info = get_info(&state.docker, &container_name).await?;
|
||||
|
||||
reply(
|
||||
json!({
|
||||
"name": container_name,
|
||||
"info": info,
|
||||
}),
|
||||
GetOneTemplate {
|
||||
container_name,
|
||||
info,
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
use crate::{
|
||||
http::{
|
||||
error::AppError,
|
||||
response::{reply, HandlerResponse},
|
||||
},
|
||||
node::{
|
||||
container::ContainerInfo,
|
||||
nix::parse_arion_compose,
|
||||
stack::{
|
||||
check_compose, command, commit_compose, create_new, get_compose, get_containers,
|
||||
remove, 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_folder: String,
|
||||
stack_name: String,
|
||||
file_contents: String,
|
||||
containers: Vec<ContainerInfo>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "stack/new-form.html")]
|
||||
struct CreateTemplate {}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "stack/delete-one.html")]
|
||||
struct ConfirmDeleteTemplate {
|
||||
stack_name: String,
|
||||
}
|
||||
|
||||
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 info = parse_arion_compose(&file_contents)?;
|
||||
let containers = get_containers(&state.docker, &info.project).await?;
|
||||
|
||||
reply(
|
||||
json!({
|
||||
"folder": stack_name,
|
||||
"name": info.project,
|
||||
"file": file_contents,
|
||||
"containers": containers,
|
||||
}),
|
||||
GetOneTemplate {
|
||||
stack_folder: stack_name,
|
||||
stack_name: info.project,
|
||||
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)
|
||||
}
|
||||
|
||||
async fn confirm_deletion_page(Path(stack_name): Path<String>) -> impl IntoResponse {
|
||||
ConfirmDeleteTemplate { stack_name }
|
||||
}
|
||||
|
||||
async fn delete_stack(
|
||||
Path(stack_name): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Redirect, AppError> {
|
||||
remove(
|
||||
&state.stack_dir,
|
||||
&state.arion_bin,
|
||||
state.repository,
|
||||
&stack_name,
|
||||
)
|
||||
.await?;
|
||||
Ok(Redirect::to("/"))
|
||||
}
|
||||
|
||||
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))
|
||||
.route(
|
||||
"/:stack/delete",
|
||||
get(confirm_deletion_page).post(delete_stack),
|
||||
)
|
||||
}
|
11
src/route/stack/check.rs
Normal file
11
src/route/stack/check.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use axum::{extract::State, http::StatusCode};
|
||||
|
||||
use crate::{http::error::AppError, node::stack::check_compose, AppState};
|
||||
|
||||
pub(super) 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)
|
||||
}
|
35
src/route/stack/create.rs
Normal file
35
src/route/stack/create.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use askama::Template;
|
||||
use askama_axum::IntoResponse;
|
||||
use axum::{extract::State, response::Redirect, Form};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
http::error::AppError,
|
||||
node::stack::{check_compose, create_new},
|
||||
AppState,
|
||||
};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "stack/new-form.html")]
|
||||
struct CreateTemplate {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct CreateStackForm {
|
||||
source: String,
|
||||
}
|
||||
|
||||
pub(super) async fn create_stack(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<CreateStackForm>,
|
||||
) -> Result<Redirect, AppError> {
|
||||
// Make sure body is is ok
|
||||
let name = check_compose(&state.arion_bin, &form.source).await?;
|
||||
|
||||
create_new(state.repository, &name, &form.source).await?;
|
||||
|
||||
Ok(Redirect::to(format!("/stack/{}/", name).as_str()))
|
||||
}
|
||||
|
||||
pub(super) async fn new_stack_page() -> impl IntoResponse {
|
||||
CreateTemplate {}
|
||||
}
|
32
src/route/stack/delete.rs
Normal file
32
src/route/stack/delete.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use askama::Template;
|
||||
use askama_axum::IntoResponse;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::Redirect,
|
||||
};
|
||||
|
||||
use crate::{http::error::AppError, node::stack::remove, AppState};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "stack/delete-one.html")]
|
||||
struct ConfirmDeleteTemplate {
|
||||
stack_name: String,
|
||||
}
|
||||
|
||||
pub(super) async fn confirm_deletion_page(Path(stack_name): Path<String>) -> impl IntoResponse {
|
||||
ConfirmDeleteTemplate { stack_name }
|
||||
}
|
||||
|
||||
pub(super) async fn delete_stack(
|
||||
Path(stack_name): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Redirect, AppError> {
|
||||
remove(
|
||||
&state.stack_dir,
|
||||
&state.arion_bin,
|
||||
state.repository,
|
||||
&stack_name,
|
||||
)
|
||||
.await?;
|
||||
Ok(Redirect::to("/"))
|
||||
}
|
62
src/route/stack/edit.rs
Normal file
62
src/route/stack/edit.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Redirect,
|
||||
Form,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
http::error::AppError,
|
||||
node::stack::{check_compose, command, commit_compose, write_compose, StackCommand},
|
||||
AppState,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct EditStackForm {
|
||||
source: String,
|
||||
commit_message: String,
|
||||
}
|
||||
|
||||
pub(super) 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("./"))
|
||||
}
|
54
src/route/stack/mod.rs
Normal file
54
src/route/stack/mod.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::Redirect,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
|
||||
use crate::{http::error::AppError, node::stack::command, node::stack::StackCommand, AppState};
|
||||
|
||||
use axum::routing::get;
|
||||
|
||||
mod check;
|
||||
mod create;
|
||||
mod delete;
|
||||
mod edit;
|
||||
mod read;
|
||||
|
||||
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(create::new_stack_page).post(create::create_stack),
|
||||
)
|
||||
.route("/_/check", post(check::check_stack_file))
|
||||
.route(
|
||||
"/:stack",
|
||||
get(|Path(stack_name): Path<String>| async move {
|
||||
Redirect::permanent(format!("{}/", &stack_name).as_str())
|
||||
}),
|
||||
)
|
||||
.route("/:stack/", get(read::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::edit_stack))
|
||||
.route(
|
||||
"/:stack/delete",
|
||||
get(delete::confirm_deletion_page).post(delete::delete_stack),
|
||||
)
|
||||
//.route("/:stack/history", get(get_stack_history))
|
||||
}
|
46
src/route/stack/read.rs
Normal file
46
src/route/stack/read.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use askama::Template;
|
||||
use axum::extract::{Path, State};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
http::response::{reply, HandlerResponse},
|
||||
node::{
|
||||
container::ContainerInfo,
|
||||
nix::parse_arion_compose,
|
||||
stack::{get_compose, get_containers},
|
||||
},
|
||||
AppState,
|
||||
};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "stack/get-one.html")]
|
||||
struct GetOneTemplate {
|
||||
stack_folder: String,
|
||||
stack_name: String,
|
||||
file_contents: String,
|
||||
containers: Vec<ContainerInfo>,
|
||||
}
|
||||
|
||||
pub(super) 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 info = parse_arion_compose(&file_contents)?;
|
||||
let containers = get_containers(&state.docker, &info.project).await?;
|
||||
|
||||
reply(
|
||||
json!({
|
||||
"folder": stack_name,
|
||||
"name": info.project,
|
||||
"file": file_contents,
|
||||
"containers": containers,
|
||||
}),
|
||||
GetOneTemplate {
|
||||
stack_folder: stack_name,
|
||||
stack_name: info.project,
|
||||
file_contents,
|
||||
containers: containers.iter().map(|c| c.clone().into()).collect(),
|
||||
},
|
||||
)
|
||||
}
|
|
@ -139,3 +139,18 @@ pre {
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5ch;
|
||||
|
||||
& form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& button {
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -87,7 +87,6 @@
|
|||
}
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 1rem;
|
||||
|
|
|
@ -73,7 +73,9 @@
|
|||
</article>
|
||||
</section>
|
||||
<section class="stack-list">
|
||||
<h2>Stacks <form action="/stack/_/new"><button>+</button></form>
|
||||
<h2>
|
||||
Stacks
|
||||
<form action="/stack/_/new"><button>+</button></form>
|
||||
</h2>
|
||||
<table class="table full-width">
|
||||
<thead>
|
||||
|
@ -135,21 +137,6 @@
|
|||
</main>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5ch;
|
||||
|
||||
& form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& button {
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containers {
|
||||
& .status {
|
||||
text-align: center;
|
||||
|
|
|
@ -7,11 +7,31 @@
|
|||
<header>
|
||||
<h1>Stack details for <span class="stack-name">{{stack_name}}</span></h1>
|
||||
<div class="actions">
|
||||
<form action="./start" method="POST"><button type="submit">Start</button></form>
|
||||
<form action="./restart" method="POST"><button type="submit">Restart</button></form>
|
||||
<form action="./stop" method="POST"><button type="submit">Stop</button></form>
|
||||
<form action="./down" method="POST"><button type="submit">Down</button></form>
|
||||
<form action="./delete" method="GET"><button type="submit">Delete</button></form>
|
||||
<form action="./start" method="POST">
|
||||
<button title="Start all containers" type="submit">
|
||||
Start
|
||||
</button>
|
||||
</form>
|
||||
<form action="./restart" method="POST">
|
||||
<button title="Restart containers" type="submit">
|
||||
Restart
|
||||
</button>
|
||||
</form>
|
||||
<form action="./stop" method="POST">
|
||||
<button title="Stop all containers" type="submit">
|
||||
Stop
|
||||
</button>
|
||||
</form>
|
||||
<form action="./down" method="POST">
|
||||
<button title="Stop and remove all resources" type="submit">
|
||||
Down
|
||||
</button>
|
||||
</form>
|
||||
<form action="./delete" method="GET">
|
||||
<button title="Stop and delete everything" type="submit">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
<section class="container-list">
|
||||
|
@ -36,7 +56,10 @@
|
|||
</table>
|
||||
</section>
|
||||
<section class="editor">
|
||||
<h2>Editor</h2>
|
||||
<h2>
|
||||
Editor
|
||||
<form action="./history"><button title="View past versions">History</button></form>
|
||||
</h2>
|
||||
<form method="POST" action="./edit" id="editor-form">
|
||||
<div class="error"></div>
|
||||
<textarea name="source" id="editor">{{file_contents}}</textarea>
|
||||
|
@ -59,10 +82,6 @@
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.containers {
|
||||
& .status {
|
||||
text-align: center;
|
||||
|
|
Reference in a new issue