This commit is contained in:
Hamcha 2023-11-22 10:59:51 +01:00
parent 40202f6889
commit 53dd75c7b4
10 changed files with 315 additions and 124 deletions

70
Cargo.lock generated
View file

@ -166,7 +166,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core",
"bitflags",
"bitflags 1.3.2",
"bytes",
"futures-util",
"http",
@ -243,6 +243,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bollard"
version = "0.15.0"
@ -303,6 +309,7 @@ version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"jobserver",
"libc",
]
@ -517,6 +524,19 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "git2"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd"
dependencies = [
"bitflags 2.4.1",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
name = "h2"
version = "0.3.22"
@ -707,6 +727,15 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jobserver"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.65"
@ -728,12 +757,36 @@ version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "libgit2-sys"
version = "0.16.1+1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libz-sys"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "lock_api"
version = "0.4.11"
@ -943,6 +996,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -993,7 +1052,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
@ -1193,6 +1252,7 @@ dependencies = [
"clap",
"dotenvy",
"futures-util",
"git2",
"rnix",
"serde",
"serde_json",
@ -1524,6 +1584,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"

View file

@ -12,10 +12,11 @@ bollard = { version = "0.15", features = ["time"] }
clap = { version = "4", features = ["env", "derive"] }
dotenvy = "0.15"
futures-util = "0.3"
rnix = "0.11.0"
git2 = { version = "0.18", default-features = false }
rnix = "0.11"
serde = "1"
serde_json = "1"
sysinfo = "0.29.10"
sysinfo = "0.29"
thiserror = "1"
time = { version = "0.3", features = ["serde"] }
tokio = { version = "1", features = ["full"] }

View file

@ -1,9 +1,11 @@
use crate::http::response::response_interceptor;
use anyhow::Result;
use anyhow::{anyhow, Result};
use axum::middleware::from_fn;
use bollard::Docker;
use clap::Parser;
use node::git::{GitConfig, ThreadSafeRepository};
use std::{net::SocketAddr, path::PathBuf};
use tokio::fs;
mod http;
mod nix;
@ -29,6 +31,13 @@ struct Args {
default_value = "/run/current-system/sw/bin/arion"
)]
arion_binary: PathBuf,
#[arg(
long = "git-author",
default_value = "staxman <staxman@example.tld>",
env = "STAX_GIT_AUTHOR"
)]
git_author: String,
}
#[derive(Clone)]
@ -36,6 +45,8 @@ pub struct AppState {
pub stack_dir: PathBuf,
pub arion_bin: PathBuf,
pub docker: Docker,
pub gitconfig: GitConfig,
pub repository: ThreadSafeRepository,
}
#[tokio::main]
@ -54,10 +65,22 @@ async fn main() -> Result<()> {
let args = Args::parse();
tracing::info!("listening on {}", &args.bind);
let (author_name, author_email) = parse_author(&args.git_author)?;
let gitconfig = GitConfig {
author_name,
author_email,
};
// 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 state = AppState {
stack_dir: args.stack_dir,
arion_bin: args.arion_binary,
docker,
gitconfig,
repository,
};
let app = route::router()
@ -70,3 +93,15 @@ async fn main() -> Result<()> {
Ok(())
}
fn parse_author(git_author: &str) -> Result<(String, String)> {
match git_author.split_once('<') {
Some((name, email)) => Ok((
name.trim().to_string(),
email.trim_end_matches(">").trim().to_string(),
)),
None => Err(anyhow!(
"invalid git author format (email must be specified)"
)),
}
}

73
src/node/git.rs Normal file
View file

@ -0,0 +1,73 @@
use anyhow::{anyhow, Result};
use git2::{ErrorCode, IndexAddOption, Repository, Signature};
use std::{
path::Path,
sync::{Arc, Mutex},
};
use tracing::info;
#[derive(Clone)]
pub struct ThreadSafeRepository {
pub inner: Arc<Mutex<Repository>>,
}
#[derive(Clone)]
pub struct GitConfig {
pub author_name: String,
pub author_email: String,
}
impl ThreadSafeRepository {
pub fn ensure_repository(config: &GitConfig, path: &Path) -> Result<Self> {
let res = Repository::open(path);
match res {
Ok(repository) => Ok(repository.into()),
Err(err) => match err.code() {
ErrorCode::NotFound => ThreadSafeRepository::create_repository(config, path),
_ => Err(anyhow!(err)),
},
}
}
fn create_repository(config: &GitConfig, path: &Path) -> Result<ThreadSafeRepository> {
// Create repository
let repo = Repository::init(path)?;
// Commit all existing files
let mut index = repo.index()?;
index.add_all(["*/*.nix"].iter(), IndexAddOption::DEFAULT, None)?;
let oid = index.write_tree()?;
let tree = repo.find_tree(oid)?;
// 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(&config.author_name, &config.author_email)?;
let commit_id = repo.commit(
Some("HEAD"),
&signature,
&signature,
"Commit message",
&tree,
&[],
)?;
drop(tree);
info!(
commit_id = commit_id.to_string(),
"Repository initialized with base commit"
);
Ok(repo.into())
}
}
impl From<Repository> for ThreadSafeRepository {
fn from(value: Repository) -> Self {
Self {
inner: Arc::new(Mutex::new(value)),
}
}
}

View file

@ -1,5 +1,6 @@
pub mod container;
pub mod error;
pub mod git;
pub mod nix;
pub mod stack;
pub mod system;

View file

@ -1,8 +1,4 @@
use super::{
container::ContainerInfo,
error::StackError,
nix::{parse_arion_compose, StackComposeInfo},
};
use super::{container::ContainerInfo, error::StackError, nix::parse_arion_compose};
use crate::http::error::Result;
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
use serde::Serialize;

View file

@ -25,6 +25,7 @@
background-color: var(--background);
color: var(--text);
font-family: Inter, sans-serif;
scrollbar-width: thin;
}
code {
@ -99,6 +100,30 @@
border-color: var(--link-hover);
}
}
table.table {
background-color: var(--bg-raised);
border: 3px solid #5958B1;
border-radius: 3px;
& th,
& td {
padding: 3px 5px;
}
& th {
font-weight: bold;
}
& thead {
background-color: #202248;
}
}
.full-width {
width: 100%;
flex: 1;
}
</style>
</head>

View file

@ -19,48 +19,51 @@
</header>
<section class="status-section">
<h2>Status</h2>
<dl class="table">
<dt>ID</dt>
<dd>{{ info.id }}</dd>
<dt>Status</dt>
<dd data-state="{{ info.state }}">{{ info.state }}</dd>
<dt>Image</dt>
<dd>{{ info.image }}</dd>
<dt>Image ID</dt>
<dd>{{ info.image_id }}</dd>
<dt>Created at</dt>
<dd>{{ info.created_at }}</dd>
</dl>
<dl class="list">
{% match info.volumes %}
{% when Some with (volumes) %}
<dt>Volumes</dt>
{% for volume in volumes %}
<dd>
{{ volume.source.clone().unwrap_or_default() }} → {{
volume.destination.clone().unwrap_or_default() }}
</dd>
{% endfor %}
{% when None %}
{% endmatch %}
{% match info.env %}
{% when Some with (env) %}
<dt>Environment</dt>
{% for var in env %}
<dd>{{ var }}</dd>
{% endfor %}
{% when None %}
{% endmatch %}
{% match info.labels %}
{% when Some with (labels) %}
<dt>Labels</dt>
{% for label in labels %}
<dd class="label"><span class="key">{{ label.0 }}</span> = <span class="value">{{ label.1
}}</span></dd>
{% endfor %}
{% when None %}
{% endmatch %}
</dl>
<article>
<dl class="table">
<dt>ID</dt>
<dd>{{ info.id }}</dd>
<dt>Status</dt>
<dd data-state="{{ info.state }}">{{ info.state }}</dd>
<dt>Image</dt>
<dd>{{ info.image }}</dd>
<dt>Image ID</dt>
<dd>{{ info.image_id }}</dd>
<dt>Created at</dt>
<dd>{{ info.created_at }}</dd>
</dl>
<dl class="list">
{% match info.volumes %}
{% when Some with (volumes) %}
<dt>Volumes</dt>
{% for volume in volumes %}
<dd>
{{ volume.source.clone().unwrap_or_default() }} → {{
volume.destination.clone().unwrap_or_default() }}
</dd>
{% endfor %}
{% when None %}
{% endmatch %}
{% match info.env %}
{% when Some with (env) %}
<dt>Environment</dt>
{% for var in env %}
<dd>{{ var }}</dd>
{% endfor %}
{% when None %}
{% endmatch %}
{% match info.labels %}
{% when Some with (labels) %}
<dt>Labels</dt>
{% for label in labels %}
<dd class="label"><span class="key">{{ label.0 }}</span> = <span class="value">{{
label.1
}}</span></dd>
{% endfor %}
{% when None %}
{% endmatch %}
</dl>
</article>
</section>
<section class="log-section">
<h2>Logs</h2>
@ -223,15 +226,23 @@
@media (min-width: 1000px) {
main {
overflow: hidden;
display: grid;
grid-template-areas:
"head head head"
"info log log"
"info log log";
"head head"
"info log";
grid-template-rows: 120px 1fr;
grid-template-columns: 30vw 1fr;
}
.status-section>article,
#log {
height: calc(100vh - 240px);
max-height: none;
overflow: auto;
}
}
.actions {

View file

@ -74,24 +74,43 @@
</section>
<section class="stack-list">
<h2>Stacks</h2>
<ul>
{% for stack in info.stacks %}
<li class="{% if stack.active %}active{% endif %}">
<a href="/stack/{{stack.name}}/">{{stack.name}}</a>
{% let (running, stopped) = stack.stats() %}
{% if running > 0 %}
<span class="count running">{{running}}</span>
{% endif %}
{% if stopped > 0 %}
<span class="count stopped">{{stopped}}</span>
{% endif %}
</li>
{% endfor %}
</ul>
<table class="table full-width">
<thead>
<tr>
<th colspan="2">Name</th>
<th>Service status</th>
</tr>
</thead>
<tbody>
{% for stack in info.stacks %}
<tr>
<td class="status {% if stack.active %}active{% endif %}"></td>
<td>
<a href="/stack/{{stack.name}}/">
{{stack.name}}
</a>
</td>
<td>
{% let (running, stopped) = stack.stats() %}
{% if running > 0 %}
<span class="count running"
title="{{running}} running">{{running}}</span>
{% endif %}
{% if stopped > 0 %}
<span class="count stopped"
title="{{stopped}} stopped">{{stopped}}</span>
{% endif %}
</td>
</li>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="container-list">
<h2>Containers</h2>
<table class="containers">
<table class="containers table full-width">
<thead>
<tr>
<th>Name</th>
@ -115,42 +134,7 @@
</main>
<style scoped>
ul {
margin: 0;
padding: 0;
}
li {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
gap: 0.5rem;
border-left: 5px solid #E5484D;
padding-left: 0.5ch;
&.active {
border-left-color: #33B074;
}
}
.containers {
background-color: var(--bg-raised);
border: 3px solid #5958B1;
border-radius: 3px;
& th,
& td {
padding: 3px 5px;
}
& th {
font-weight: bold;
}
& thead {
background-color: #202248;
}
& .status {
text-align: center;
font-weight: bold;
@ -173,6 +157,21 @@
.stack-list {
grid-area: stk;
& td {
padding: 4px 8px;
&.status {
width: 8px;
padding: 0;
background-color: #E5484D;
&.active {
background-color: #33B074;
}
}
}
}
.system-info {
@ -205,12 +204,13 @@
@media (min-width: 1000px) {
main {
display: grid;
gap: 1rem;
grid-template-areas:
"sys sys"
"stk con";
grid-template-rows: 100px 1fr;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr 2fr;
}
}
</style>

View file

@ -15,7 +15,7 @@
</header>
<section class="container-list">
<h2>Containers</h2>
<table class="containers">
<table class="containers table">
<thead>
<tr>
<th>Name</th>
@ -57,23 +57,6 @@
}
.containers {
background-color: var(--bg-raised);
border: 3px solid #5958B1;
border-radius: 3px;
& th,
& td {
padding: 3px 5px;
}
& th {
font-weight: bold;
}
& thead {
background-color: #202248;
}
& .status {
text-align: center;
font-weight: bold;