diff --git a/src/main.rs b/src/main.rs index bb477b0..8061581 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,6 @@ pub struct AppState { pub stack_dir: PathBuf, pub arion_bin: PathBuf, pub docker: Docker, - pub gitconfig: GitConfig, pub repository: ThreadSafeRepository, } @@ -73,13 +72,12 @@ async fn main() -> Result<()> { // Make sure stack arg exists and is properly initialized fs::create_dir_all(&args.stack_dir).await?; - let repository = ThreadSafeRepository::ensure_repository(&gitconfig, &args.stack_dir)?; + let repository = ThreadSafeRepository::ensure_repository(gitconfig, &args.stack_dir)?; let state = AppState { stack_dir: args.stack_dir, arion_bin: args.arion_binary, docker, - gitconfig, repository, }; diff --git a/src/node/git.rs b/src/node/git.rs index e02d1d1..1e3e42c 100644 --- a/src/node/git.rs +++ b/src/node/git.rs @@ -1,14 +1,12 @@ use anyhow::{anyhow, Result}; use git2::{ErrorCode, IndexAddOption, Repository, Signature}; -use std::{ - path::Path, - sync::{Arc, Mutex}, -}; +use std::path::{Path, PathBuf}; use tracing::info; #[derive(Clone)] pub struct ThreadSafeRepository { - pub inner: Arc>, + pub path: PathBuf, + pub config: GitConfig, } #[derive(Clone)] @@ -18,11 +16,14 @@ pub struct GitConfig { } impl ThreadSafeRepository { - pub fn ensure_repository(config: &GitConfig, path: &Path) -> Result { + pub fn ensure_repository(config: GitConfig, path: &Path) -> Result { let res = Repository::open(path); match res { - Ok(repository) => Ok(repository.into()), + Ok(_) => Ok(Self { + path: path.to_path_buf(), + config, + }), Err(err) => match err.code() { ErrorCode::NotFound => ThreadSafeRepository::create_repository(config, path), _ => Err(anyhow!(err)), @@ -30,7 +31,7 @@ impl ThreadSafeRepository { } } - fn create_repository(config: &GitConfig, path: &Path) -> Result { + fn create_repository(config: GitConfig, path: &Path) -> Result { // Create repository let repo = Repository::init(path)?; @@ -49,7 +50,7 @@ impl ThreadSafeRepository { Some("HEAD"), &signature, &signature, - "Commit message", + "Initializing stack repository", &tree, &[], )?; @@ -60,14 +61,41 @@ impl ThreadSafeRepository { "Repository initialized with base commit" ); - Ok(repo.into()) + Ok(Self { + path: path.to_path_buf(), + config, + }) } -} -impl From for ThreadSafeRepository { - fn from(value: Repository) -> Self { - Self { - inner: Arc::new(Mutex::new(value)), - } + fn repository(&self) -> Result { + Repository::open(&self.path) + } + + pub fn commit_file(&self, path: &Path, message: &str) -> Result<()> { + let repository = self.repository()?; + + // Commit file + let mut index = repository.index()?; + index.add_path(path)?; + let oid = index.write_tree()?; + let tree = repository.find_tree(oid)?; + let head = repository.head()?; + + // This prevents a nasty condition where the index goes all wack, + // but it's probably a mistake somewhere else + index.write()?; + + let signature = Signature::now(&self.config.author_name, &self.config.author_email)?; + repository.commit( + Some("HEAD"), + &signature, + &signature, + message, + &tree, + &[&head.peel_to_commit()?], + )?; + drop(tree); + + Ok(()) } } diff --git a/src/node/stack.rs b/src/node/stack.rs index c594bb6..4c99978 100644 --- a/src/node/stack.rs +++ b/src/node/stack.rs @@ -1,15 +1,21 @@ -use super::{container::ContainerInfo, error::StackError, nix::parse_arion_compose}; +use super::{ + container::ContainerInfo, error::StackError, git::ThreadSafeRepository, + nix::parse_arion_compose, +}; use crate::http::error::Result; use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker}; use serde::Serialize; -use std::{collections::HashMap, path::Path}; -use tokio::fs::{read_dir, try_exists}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; +use tokio::fs; use xshell::Shell; const COMPOSE_FILE: &str = "arion-compose.nix"; async fn is_stack(dir: &Path) -> Result { - Ok(try_exists(dir.join(COMPOSE_FILE)).await?) + Ok(fs::try_exists(dir.join(COMPOSE_FILE)).await?) } pub async fn get_containers(docker: &Docker, stack_name: &str) -> Result> { @@ -96,7 +102,7 @@ pub async fn list(base_dir: &Path, docker: &Docker) -> Result { .map(|c| c.clone().into()) .collect(); - let mut dirs = read_dir(base_dir).await?; + let mut dirs = fs::read_dir(base_dir).await?; let mut stacks = vec![]; while let Some(dir) = dirs.next_entry().await? { let meta = dir.metadata().await?; @@ -127,6 +133,25 @@ pub async fn list(base_dir: &Path, docker: &Docker) -> Result { Ok(NodeInfo { stacks, containers }) } +pub async fn write_compose(base_dir: &Path, stack_name: &str, contents: &str) -> Result<()> { + let dir = base_dir.join(stack_name); + if !is_stack(&dir).await? { + return Err(StackError::NotFound.into()); + } + + Ok(fs::write(dir.join(COMPOSE_FILE), contents).await?) +} + +pub fn commit_compose( + repository: ThreadSafeRepository, + stack_name: &str, + message: &str, +) -> Result<()> { + let compose_path = format!("{}/{}", stack_name, COMPOSE_FILE); + repository.commit_file(&PathBuf::from(compose_path), message)?; + Ok(()) +} + pub async fn command( base_dir: &Path, stack_name: &str, diff --git a/src/route/stack.rs b/src/route/stack.rs index e6e8018..538b98e 100644 --- a/src/route/stack.rs +++ b/src/route/stack.rs @@ -5,18 +5,23 @@ use crate::{ }, node::{ container::ContainerInfo, - stack::{command, get_compose, get_containers, StackCommand}, + stack::{ + command, commit_compose, 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}, - Router, + Form, Router, }; use futures_util::join; +use serde::Deserialize; use serde_json::json; #[derive(Template)] @@ -27,6 +32,10 @@ struct GetOneTemplate { containers: Vec, } +#[derive(Template)] +#[template(path = "stack/new-form.html")] +struct CreateTemplate {} + async fn get_one(Path(stack_name): Path, State(state): State) -> HandlerResponse { let (file_contents_res, containers_res) = join!( get_compose(&state.stack_dir, &stack_name), @@ -50,6 +59,56 @@ async fn get_one(Path(stack_name): Path, State(state): State) ) } +async fn new_stack_page() -> impl IntoResponse { + CreateTemplate {} +} + +#[derive(Deserialize)] +struct EditStackForm { + source: String, + commit_message: String, +} + +async fn edit_stack( + Path(stack_name): Path, + State(state): State, + Form(form): Form, +) -> Result { + 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", + }); + }; + + // Cleanup source (like line endings) + let source = form.source.replace("\r\n", "\n"); + + // 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("./")) as Result +} + macro_rules! stack_command { ($cmd: expr) => { move |Path(stack_name): Path, State(state): State| async move { @@ -61,6 +120,7 @@ macro_rules! stack_command { pub(super) fn router() -> Router { Router::new() + .route("/_/new", get(new_stack_page)) .route( "/:stack", get(|Path(stack_name): Path| async move { @@ -75,4 +135,5 @@ pub(super) fn router() -> Router { ) .route("/:stack/stop", post(stack_command!(StackCommand::Stop))) .route("/:stack/down", post(stack_command!(StackCommand::Down))) + .route("/:stack/edit", post(edit_stack)) } diff --git a/templates/home.html b/templates/home.html index 1dca3f0..30429e9 100644 --- a/templates/home.html +++ b/templates/home.html @@ -6,7 +6,7 @@
- +
@@ -50,7 +50,7 @@
-
Hostname
+
{% for disk in system.disks %} @@ -73,7 +73,8 @@
-

Stacks

+

Stacks
+

@@ -134,6 +135,21 @@