add check
This commit is contained in:
parent
21dd617a4b
commit
9e857e1d57
10 changed files with 212 additions and 47 deletions
65
Cargo.lock
generated
65
Cargo.lock
generated
|
@ -451,6 +451,22 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
|
@ -806,6 +822,12 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.11"
|
||||
|
@ -1108,6 +1130,19 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
|
@ -1278,13 +1313,13 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"xshell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1325,6 +1360,19 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"redox_syscall",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.1"
|
||||
|
@ -1782,18 +1830,3 @@ name = "windows_x86_64_msvc"
|
|||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "xshell"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2107fe03e558353b4c71ad7626d58ed82efaf56c54134228608893c77023ad"
|
||||
dependencies = [
|
||||
"xshell-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xshell-macros"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e2c411759b501fb9501aac2b1b2d287a6e93e5bdcf13c25306b23e1b716dd0e"
|
||||
|
|
|
@ -19,10 +19,10 @@ rnix = "0.11"
|
|||
serde = "1"
|
||||
serde_json = "1"
|
||||
sysinfo = "0.29"
|
||||
tempfile = "3"
|
||||
thiserror = "1"
|
||||
time = { version = "0.3", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
xshell = "0.2"
|
||||
|
|
|
@ -13,7 +13,7 @@ pub enum AppError {
|
|||
Client {
|
||||
status: StatusCode,
|
||||
code: &'static str,
|
||||
message: &'static str,
|
||||
message: String,
|
||||
},
|
||||
|
||||
#[error("docker error: {0}")]
|
||||
|
@ -22,9 +22,6 @@ pub enum AppError {
|
|||
#[error("file error: {0}")]
|
||||
FileError(#[from] std::io::Error),
|
||||
|
||||
#[error("shell error: {0}")]
|
||||
ShellError(#[from] xshell::Error),
|
||||
|
||||
#[error("unexpected internal error: {0}")]
|
||||
Internal(#[from] anyhow::Error),
|
||||
|
||||
|
@ -54,11 +51,6 @@ impl IntoResponse for AppError {
|
|||
code: "file-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
AppError::ShellError(err) => ErrorInfo {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: "shell-error".to_string(),
|
||||
message: err.to_string(),
|
||||
},
|
||||
AppError::Template(err) => ErrorInfo {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: "template-error".to_string(),
|
||||
|
|
|
@ -14,7 +14,7 @@ impl From<StackError> for AppError {
|
|||
StackError::NotFound => AppError::Client {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
code: "stack-not-found",
|
||||
message: "stack not found",
|
||||
message: "stack not found".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,12 +71,14 @@ impl ThreadSafeRepository {
|
|||
Repository::open(&self.path)
|
||||
}
|
||||
|
||||
pub fn commit_file(&self, path: &Path, message: &str) -> Result<()> {
|
||||
pub fn commit_files(&self, paths: &[&Path], message: &str) -> Result<()> {
|
||||
let repository = self.repository()?;
|
||||
|
||||
// Commit file
|
||||
let mut index = repository.index()?;
|
||||
index.add_path(path)?;
|
||||
for path in paths {
|
||||
index.add_path(path)?;
|
||||
}
|
||||
let oid = index.write_tree()?;
|
||||
let tree = repository.find_tree(oid)?;
|
||||
let head = repository.head()?;
|
||||
|
|
|
@ -2,17 +2,23 @@ use super::{
|
|||
container::ContainerInfo, error::StackError, git::ThreadSafeRepository,
|
||||
nix::parse_arion_compose,
|
||||
};
|
||||
use crate::http::error::Result;
|
||||
use crate::http::error::{AppError, Result};
|
||||
use axum::http::StatusCode;
|
||||
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
|
||||
use futures_util::future::try_join;
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
use tokio::fs;
|
||||
use xshell::Shell;
|
||||
|
||||
const COMPOSE_FILE: &str = "arion-compose.nix";
|
||||
const PACKAGE_FILE: &str = "arion-pkgs.nix";
|
||||
const PACKAGE_CONTENTS: &str = r#"import <nixpkgs> { system = "x86_64-linux"; }
|
||||
"#;
|
||||
|
||||
async fn is_stack(dir: &Path) -> Result<bool> {
|
||||
Ok(fs::try_exists(dir.join(COMPOSE_FILE)).await?)
|
||||
|
@ -148,26 +154,108 @@ pub fn commit_compose(
|
|||
message: &str,
|
||||
) -> Result<()> {
|
||||
let compose_path = format!("{}/{}", stack_name, COMPOSE_FILE);
|
||||
repository.commit_file(&PathBuf::from(compose_path), message)?;
|
||||
repository.commit_files(&[&PathBuf::from(compose_path)], message)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_new(
|
||||
repository: ThreadSafeRepository,
|
||||
stack_name: &str,
|
||||
source: &str,
|
||||
) -> Result<()> {
|
||||
// Calculate stack directory and create it
|
||||
let stack_path = repository.path.join(stack_name);
|
||||
fs::create_dir_all(&stack_path).await?;
|
||||
|
||||
// Create package file and compose file
|
||||
try_join(
|
||||
fs::write(stack_path.join(PACKAGE_FILE), PACKAGE_CONTENTS),
|
||||
fs::write(stack_path.join(COMPOSE_FILE), source),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Commit everything
|
||||
repository.commit_files(
|
||||
&[
|
||||
&PathBuf::from(format!("{}/{}", stack_name, PACKAGE_FILE)),
|
||||
&PathBuf::from(format!("{}/{}", stack_name, COMPOSE_FILE)),
|
||||
],
|
||||
format!("Created stack {}", stack_name).as_str(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> {
|
||||
// Check that it's a valid nix tree
|
||||
rnix::Root::parse(source)
|
||||
.ok()
|
||||
.map_err(|_| AppError::Client {
|
||||
status: StatusCode::NOT_ACCEPTABLE,
|
||||
code: "failed-nix-parse",
|
||||
message: "The provided source is not valid Nix".to_string(),
|
||||
})?;
|
||||
|
||||
// Create a temporary stack and check that it generates a YAML tree
|
||||
let dir = tempdir()?;
|
||||
let path = dir.path();
|
||||
|
||||
// Create package file and compose file
|
||||
try_join(
|
||||
fs::write(path.join(PACKAGE_FILE), PACKAGE_CONTENTS),
|
||||
fs::write(path.join(COMPOSE_FILE), source),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cmd = arion(arion_bin, path, StackCommand::Test)?;
|
||||
|
||||
dir.close()?;
|
||||
|
||||
if let CommandStatus::Failure(_, err) = cmd {
|
||||
Err(AppError::Client {
|
||||
status: StatusCode::NOT_ACCEPTABLE,
|
||||
code: "failed-arion-check",
|
||||
message: err,
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CommandStatus {
|
||||
Success(String, String),
|
||||
Failure(String, String),
|
||||
}
|
||||
|
||||
fn arion(arion_bin: &Path, path: &Path, action: StackCommand) -> Result<CommandStatus> {
|
||||
let output = Command::new(arion_bin)
|
||||
.args(action.command())
|
||||
.current_dir(path)
|
||||
.output()?;
|
||||
|
||||
// Convert stdout and stderr to String
|
||||
let stdout_str = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr_str = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
if output.status.success() {
|
||||
Ok(CommandStatus::Success(stdout_str, stderr_str))
|
||||
} else {
|
||||
Ok(CommandStatus::Failure(stdout_str, stderr_str))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn command(
|
||||
base_dir: &Path,
|
||||
stack_name: &str,
|
||||
arion_bin: &Path,
|
||||
action: StackCommand,
|
||||
) -> Result<()> {
|
||||
) -> Result<CommandStatus> {
|
||||
let dir = base_dir.join(stack_name);
|
||||
if !is_stack(&dir).await? {
|
||||
return Err(StackError::NotFound.into());
|
||||
}
|
||||
|
||||
let sh = Shell::new()?;
|
||||
sh.change_dir(dir);
|
||||
sh.cmd(arion_bin).args(action.command()).run()?;
|
||||
|
||||
Ok(())
|
||||
arion(arion_bin, &dir, action)
|
||||
}
|
||||
|
||||
pub enum StackCommand {
|
||||
|
@ -175,6 +263,7 @@ pub enum StackCommand {
|
|||
Start,
|
||||
Stop,
|
||||
Restart,
|
||||
Test,
|
||||
}
|
||||
|
||||
impl StackCommand {
|
||||
|
@ -184,6 +273,7 @@ impl StackCommand {
|
|||
StackCommand::Start => &["up", "-d"],
|
||||
StackCommand::Stop => &["stop"],
|
||||
StackCommand::Restart => &["restart"],
|
||||
StackCommand::Test => &["config"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ use crate::{
|
|||
node::{
|
||||
container::ContainerInfo,
|
||||
stack::{
|
||||
command, commit_compose, get_compose, get_containers, write_compose, StackCommand,
|
||||
check_compose, command, commit_compose, create_new, get_compose, get_containers,
|
||||
write_compose, StackCommand,
|
||||
},
|
||||
},
|
||||
AppState,
|
||||
|
@ -63,6 +64,21 @@ 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> {
|
||||
create_new(state.repository, &form.name, &form.source).await?;
|
||||
|
||||
Ok(Redirect::to(format!("/stack/{}/", form.name).as_str()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditStackForm {
|
||||
source: String,
|
||||
|
@ -84,7 +100,7 @@ async fn edit_stack(
|
|||
return Err(AppError::Client {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
code: "invalid-source",
|
||||
message: "provided stack source is empty",
|
||||
message: "provided stack source is empty".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -106,7 +122,15 @@ async fn edit_stack(
|
|||
)
|
||||
.await?;
|
||||
|
||||
Ok(Redirect::to("./")) as Result<Redirect, AppError>
|
||||
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 {
|
||||
|
@ -120,7 +144,8 @@ macro_rules! stack_command {
|
|||
|
||||
pub(super) fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/_/new", get(new_stack_page))
|
||||
.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 {
|
||||
|
|
|
@ -2,6 +2,8 @@ import "../vendor/ace/ace.js";
|
|||
import { findNearestParent } from "./node-utils.mjs";
|
||||
|
||||
export default class Editor {
|
||||
editor;
|
||||
|
||||
/**
|
||||
* Create a new editor
|
||||
* @param {string} elementID ID of element to replace with the editor
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
<div class="row">
|
||||
<input style="flex:1" name="commit_message" type="text"
|
||||
placeholder="What did you change?" />
|
||||
<div id="editor-extra"></div>
|
||||
<button class="wide" type="submit">Deploy</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -104,7 +105,27 @@
|
|||
<script type="module">
|
||||
import Editor from "/static/js/ace.mjs";
|
||||
|
||||
new Editor("editor");
|
||||
const editor = new Editor("editor");
|
||||
|
||||
/* Add extra buttons */
|
||||
const extraContainer = document.getElementById("editor-extra");
|
||||
|
||||
/* Check button */
|
||||
const checkButton = document.createElement("button");
|
||||
checkButton.appendChild(document.createTextNode("Check"));
|
||||
checkButton.type = "button";
|
||||
checkButton.addEventListener("click", async () => {
|
||||
const body = editor.editor.getValue();
|
||||
const response = await fetch("/stack/_/check", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body
|
||||
});
|
||||
})
|
||||
extraContainer.appendChild(checkButton);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -8,11 +8,11 @@
|
|||
<h1>New stack</h1>
|
||||
</header>
|
||||
<section class="editor">
|
||||
<form method="POST" action="./edit" id="editor-form">
|
||||
<form method="POST" id="editor-form">
|
||||
<div class="row">
|
||||
<input style="flex:1" name="name" type="text" placeholder="Stack name" />
|
||||
<input style="flex:1" name="name" type="text" placeholder="Stack name" required />
|
||||
</div>
|
||||
<textarea name="source" id="editor">{}</textarea>
|
||||
<textarea name="source" id="editor" required>{}</textarea>
|
||||
<div class="row">
|
||||
<button type="submit">Create & Deploy</button>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<script type="module">
|
||||
import Editor from "/static/js/ace.mjs";
|
||||
|
||||
new Editor("editor");
|
||||
const editor = new Editor("editor");
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
Reference in a new issue