begone broken nix

This commit is contained in:
Hamcha 2023-11-25 00:45:54 +01:00
parent 9e857e1d57
commit 3f62e8302e
8 changed files with 134 additions and 28 deletions

View file

@ -190,10 +190,10 @@ pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> {
// Check that it's a valid nix tree // Check that it's a valid nix tree
rnix::Root::parse(source) rnix::Root::parse(source)
.ok() .ok()
.map_err(|_| AppError::Client { .map_err(|err| AppError::Client {
status: StatusCode::NOT_ACCEPTABLE, status: StatusCode::NOT_ACCEPTABLE,
code: "failed-nix-parse", code: "failed-nix-parse",
message: "The provided source is not valid Nix".to_string(), message: format!("Parse error: {}", err),
})?; })?;
// Create a temporary stack and check that it generates a YAML tree // Create a temporary stack and check that it generates a YAML tree
@ -215,7 +215,7 @@ pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> {
Err(AppError::Client { Err(AppError::Client {
status: StatusCode::NOT_ACCEPTABLE, status: StatusCode::NOT_ACCEPTABLE,
code: "failed-arion-check", code: "failed-arion-check",
message: err, message: format!("Arion {}", err),
}) })
} else { } else {
Ok(()) Ok(())

View file

@ -74,6 +74,9 @@ async fn create_stack(
State(state): State<AppState>, State(state): State<AppState>,
Form(form): Form<CreateStackForm>, Form(form): Form<CreateStackForm>,
) -> Result<Redirect, AppError> { ) -> 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?; create_new(state.repository, &form.name, &form.source).await?;
Ok(Redirect::to(format!("/stack/{}/", form.name).as_str())) Ok(Redirect::to(format!("/stack/{}/", form.name).as_str()))
@ -107,6 +110,9 @@ async fn edit_stack(
// Cleanup source (like line endings) // Cleanup source (like line endings)
let source = form.source.replace("\r\n", "\n"); 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 file
write_compose(&state.stack_dir, &stack_name, &source).await?; write_compose(&state.stack_dir, &stack_name, &source).await?;

View file

@ -17,6 +17,15 @@
} }
} }
} }
& .error {
border-radius: 3px;
background-color: #500F1C;
color: #FF9592;
padding: 4px 8px;
display: none;
}
} }
#editor { #editor {
@ -28,4 +37,12 @@
min-height: 50vh; min-height: 50vh;
border: 3px solid #5958B1; border: 3px solid #5958B1;
border-radius: 3px; border-radius: 3px;
&.checked {
border-color: #46A758;
}
&.err {
border-color: #E5484D;
}
} }

View file

@ -121,4 +121,9 @@ textarea {
background-color: var(--bg-raised); background-color: var(--bg-raised);
color: var(--text); color: var(--text);
padding: 4px; padding: 4px;
}
pre {
margin: 0;
padding: 0;
} }

47
static/js/compose.mjs Normal file
View file

@ -0,0 +1,47 @@
/**
* Checks if a specificied source is a valid arion-compose.nix file
* @param {string} source Source to check
* @returns {Promise<CheckResult>} Result of the check
*/
export async function check_compose(source) {
const response = await fetch("/stack/_/check", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "text/plain",
},
body: source
});
if (response.ok) {
return { ok: true };
} else {
return { ok: false, error: await response.json() };
}
}
/**
* @typedef {Object} APIError
* @property {string} code - Error code
*/
/**
* @typedef {Object} CheckSuccess
* @property { true } ok - Indicates that the check was successful
* /
/**
* @typedef {Object} APIError
* @property {string} code - Unique error code
* @property {string} message - Human readable error details
*/
/**
* @typedef {Object} CheckFailure
* @property {false} ok - Indicates that the check has failed
* @property {APIError} error - Info about the encountered error
*/
/**
* @typedef {CheckSuccess | CheckFailure} CheckResult
*/

View file

@ -0,0 +1,45 @@
import Editor from "../ace.mjs";
import { check_compose } from "/static/js/compose.mjs";
/**
* Makes the form require the stack to be checked before submitting
* @param {HTMLFormElement} form
* @param {Editor} editor
*/
export function add_check(form, editor) {
form.addEventListener("submit", (ev) => {
ev.preventDefault();
check_stack(editor).then((result) => {
if (result) {
form.submit();
}
})
return false;
});
}
/**
* Runs the check function and updates some DOM elements
* @param {Editor} editor Editor instance
* @returns true if the stack is valid, false otherwise
*/
export async function check_stack(editor) {
const source = editor.editor.getValue();
const check_result = await check_compose(source);
const editorEl = document.querySelector(".ace_editor");
const editorErrorEl = document.querySelector("#editor-form .error");
editorEl.classList.remove("err", "checked");
editorErrorEl.style.display = "block";
editorErrorEl.classList.add("pending");
editorErrorEl.innerHTML = "Checking...";
if (check_result.ok) {
editorEl.classList.add("checked");
editorErrorEl.style.display = "";
} else {
editorEl.classList.add("err");
editorErrorEl.classList.remove("pending");
editorErrorEl.innerHTML = check_result.error.message;
}
return check_result.ok;
}

View file

@ -37,6 +37,7 @@
<section class="editor"> <section class="editor">
<h2>Editor</h2> <h2>Editor</h2>
<form method="POST" action="./edit" id="editor-form"> <form method="POST" action="./edit" id="editor-form">
<div class="error"></div>
<textarea name="source" id="editor">{{file_contents}}</textarea> <textarea name="source" id="editor">{{file_contents}}</textarea>
<div class="row"> <div class="row">
<input style="flex:1" name="commit_message" type="text" <input style="flex:1" name="commit_message" type="text"
@ -60,11 +61,6 @@
text-transform: uppercase; text-transform: uppercase;
} }
pre {
margin: 0;
padding: 0;
}
.containers { .containers {
& .status { & .status {
text-align: center; text-align: center;
@ -101,31 +97,15 @@
} }
</style> </style>
<script type="module"> <script type="module">
import Editor from "/static/js/ace.mjs"; import Editor from "/static/js/ace.mjs";
import { add_check } from "/static/js/enhancements/check.mjs";
const editor = new Editor("editor"); const editor = new Editor("editor");
/* Add extra buttons */ /* Enforce check pre-submit */
const extraContainer = document.getElementById("editor-extra"); const form = document.getElementById("editor-form");
add_check(form, editor);
/* 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> </script>
{% endblock %} {% endblock %}

View file

@ -12,6 +12,7 @@
<div class="row"> <div class="row">
<input style="flex:1" name="name" type="text" placeholder="Stack name" required /> <input style="flex:1" name="name" type="text" placeholder="Stack name" required />
</div> </div>
<div class="error"></div>
<textarea name="source" id="editor" required>{}</textarea> <textarea name="source" id="editor" required>{}</textarea>
<div class="row"> <div class="row">
<button type="submit">Create & Deploy</button> <button type="submit">Create & Deploy</button>
@ -31,8 +32,13 @@
<script type="module"> <script type="module">
import Editor from "/static/js/ace.mjs"; import Editor from "/static/js/ace.mjs";
import { add_check } from "/static/js/enhancements/check.mjs";
const editor = new Editor("editor"); const editor = new Editor("editor");
/* Enforce check pre-submit */
const form = document.getElementById("editor-form");
add_check(form, editor);
</script> </script>
{% endblock %} {% endblock %}