stack editing
This commit is contained in:
parent
53dd75c7b4
commit
50f2c97d0c
7 changed files with 219 additions and 31 deletions
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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<Mutex<Repository>>,
|
||||
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<Self> {
|
||||
pub fn ensure_repository(config: GitConfig, path: &Path) -> Result<Self> {
|
||||
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<ThreadSafeRepository> {
|
||||
fn create_repository(config: GitConfig, path: &Path) -> Result<Self> {
|
||||
// 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<Repository> for ThreadSafeRepository {
|
||||
fn from(value: Repository) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(value)),
|
||||
}
|
||||
fn repository(&self) -> Result<Repository, git2::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<bool> {
|
||||
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<Vec<ContainerSummary>> {
|
||||
|
@ -96,7 +102,7 @@ pub async fn list(base_dir: &Path, docker: &Docker) -> Result<NodeInfo> {
|
|||
.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<NodeInfo> {
|
|||
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,
|
||||
|
|
|
@ -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<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),
|
||||
|
@ -50,6 +59,56 @@ async fn get_one(Path(stack_name): Path<String>, State(state): State<AppState>)
|
|||
)
|
||||
}
|
||||
|
||||
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<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",
|
||||
});
|
||||
};
|
||||
|
||||
// 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<Redirect, AppError>
|
||||
}
|
||||
|
||||
macro_rules! stack_command {
|
||||
($cmd: expr) => {
|
||||
move |Path(stack_name): Path<String>, State(state): State<AppState>| async move {
|
||||
|
@ -61,6 +120,7 @@ macro_rules! stack_command {
|
|||
|
||||
pub(super) fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/_/new", get(new_stack_page))
|
||||
.route(
|
||||
"/:stack",
|
||||
get(|Path(stack_name): Path<String>| async move {
|
||||
|
@ -75,4 +135,5 @@ pub(super) fn router() -> Router<AppState> {
|
|||
)
|
||||
.route("/:stack/stop", post(stack_command!(StackCommand::Stop)))
|
||||
.route("/:stack/down", post(stack_command!(StackCommand::Down)))
|
||||
.route("/:stack/edit", post(edit_stack))
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<main>
|
||||
<section class="system-info">
|
||||
<article>
|
||||
<table>
|
||||
<table class="full-width">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
|
@ -50,7 +50,7 @@
|
|||
|
||||
</article>
|
||||
<article>
|
||||
<table>
|
||||
<table class="full-width">
|
||||
<tbody>
|
||||
|
||||
{% for disk in system.disks %}
|
||||
|
@ -73,7 +73,8 @@
|
|||
</article>
|
||||
</section>
|
||||
<section class="stack-list">
|
||||
<h2>Stacks</h2>
|
||||
<h2>Stacks <form action="/stack/_/new"><button>+</button></form>
|
||||
</h2>
|
||||
<table class="table full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -134,6 +135,21 @@
|
|||
</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;
|
||||
|
@ -162,7 +178,7 @@
|
|||
padding: 4px 8px;
|
||||
|
||||
&.status {
|
||||
width: 8px;
|
||||
width: 6px;
|
||||
padding: 0;
|
||||
background-color: #E5484D;
|
||||
|
||||
|
@ -178,6 +194,7 @@
|
|||
grid-area: sys;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
& th {
|
||||
font-weight: bold;
|
||||
|
@ -202,6 +219,11 @@
|
|||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.system-info {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
|
|
@ -36,7 +36,14 @@
|
|||
</section>
|
||||
<section class="editor">
|
||||
<h2>Editor</h2>
|
||||
<textarea id="editor">{{file_contents}}</textarea>
|
||||
<form method="POST" action="./edit" id="editor-form">
|
||||
<textarea name="source" id="editor">{{file_contents}}</textarea>
|
||||
<div class="row">
|
||||
<input style="flex:1" name="commit_message" type="text"
|
||||
placeholder="What did you change?" />
|
||||
<button class="wide" type="submit">Deploy</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
@ -77,6 +84,42 @@
|
|||
color: #E796F3;
|
||||
}
|
||||
|
||||
.editor form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5ch;
|
||||
|
||||
& .row {
|
||||
display: flex;
|
||||
gap: 0.5ch;
|
||||
|
||||
& input,
|
||||
& button {
|
||||
font-size: 13pt;
|
||||
padding: 6px 10px;
|
||||
|
||||
&.wide {
|
||||
padding: 6px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border: 1px solid #5958B1;
|
||||
border-radius: 3px;
|
||||
background-color: var(--bg-raised);
|
||||
color: var(--text);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#editor {
|
||||
border-width: 3px;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.ace_editor {
|
||||
min-height: 50vh;
|
||||
border: 3px solid #5958B1;
|
||||
|
@ -108,8 +151,13 @@
|
|||
fontFamily: "Iosevka Web",
|
||||
fontSize: "12pt"
|
||||
});
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
editor.setKeyboardHandler("ace/keyboard/vim");
|
||||
editor.session.setMode("ace/mode/nix");
|
||||
|
||||
document.getElementById("editor-form").addEventListener("formdata", (ev) => {
|
||||
ev.formData.set("source", editor.getValue());
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
6
templates/stack/new-form.html
Normal file
6
templates/stack/new-form.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New stack{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
Reference in a new issue