some naive handling of project renaming

This commit is contained in:
Hamcha 2023-11-25 11:51:41 +01:00
parent f4915daa0c
commit e3fecbee95
7 changed files with 86 additions and 28 deletions

View File

@ -8,7 +8,7 @@ use bollard::{
Docker,
};
use serde::Serialize;
use std::collections::HashMap;
use std::{collections::HashMap, path::Path};
use time::OffsetDateTime;
#[derive(Serialize)]
@ -31,6 +31,19 @@ impl ContainerInfo {
.and_then(|lab| lab.get("com.docker.compose.project").cloned())
}
pub fn stack_folder(&self) -> Option<String> {
self.labels
.clone()
.and_then(|lab| lab.get("com.docker.compose.project.working_dir").cloned())
.and_then(|workdir| {
Path::new(&workdir)
.components()
.last()
.and_then(|part| part.as_os_str().to_str())
.map(|s| s.to_string())
})
}
pub fn running(&self) -> bool {
self.state == "running"
}

View File

@ -3,6 +3,7 @@ use anyhow::{anyhow, Result};
use std::collections::HashSet;
pub struct StackComposeInfo {
pub project: String,
pub services: Vec<String>,
}
@ -11,6 +12,11 @@ pub fn parse_arion_compose(file: &str) -> Result<StackComposeInfo> {
let expr = ast.expr().ok_or_else(|| anyhow!("invalid nix root"))?;
let flattened = flatten_set(expr)?;
let project = flattened["project\0name"]
.clone()
.trim_matches('"')
.to_string();
let services = flattened
.iter()
.filter_map(|(key, _)| {
@ -22,5 +28,43 @@ pub fn parse_arion_compose(file: &str) -> Result<StackComposeInfo> {
.into_iter()
.collect();
Ok(StackComposeInfo { services })
Ok(StackComposeInfo { project, services })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_compose() {
let source = r#"{
project.name = "traefik";
services.router = {
service.image = "traefik:v3.0";
service.restart = "always";
service.volumes = [
"${toString ./.}/traefik.toml:/traefik.toml"
"${toString ./.}/acme.json:/acme.json"
"${toString ./.}/rules.d:/rules.d"
"/var/run/docker.sock:/var/run/docker.sock:ro"
];
service.ports = [
"80:80"
];
service.labels = {
"traefik.enable" = "true";
"traefik.http.routers.traefik_dashboard.rule" = "Host(`traefik.uchunext.local`)";
"traefik.http.routers.traefik_dashboard.service" = "api@internal";
"traefik.http.routers.traefik_dashboard.entrypoints" = "web";
"traefik.http.services.dashboard.loadbalancer.server.port" = "8080";
};
service.networks = ["web"];
};
networks.web.external = true;
}"#;
let info = parse_arion_compose(source).unwrap();
assert_eq!(info.project, "traefik");
assert_eq!(info.services[0], "router");
}
}

View File

@ -57,6 +57,7 @@ pub struct ServiceInfo {
#[derive(Serialize)]
pub struct StackInfo {
pub folder: String,
pub name: String,
pub active: bool,
pub services: Vec<ServiceInfo>,
@ -116,19 +117,21 @@ pub async fn list(base_dir: &Path, docker: &Docker) -> Result<NodeInfo> {
continue;
}
if is_stack(&dir.path()).await? {
let name = dir.file_name().to_string_lossy().to_string();
let folder = dir.file_name().to_string_lossy().to_string();
let compose_file = get_compose(base_dir, &folder).await?;
let info = parse_arion_compose(&compose_file)?;
let name = info.project;
// Check status by analyzing containers
let active = containers
.iter()
.any(|cont| cont.state == "running" && cont.stack() == Some(name.clone()));
let compose_file = get_compose(base_dir, &name).await?;
let info = parse_arion_compose(&compose_file)?;
let services = info
.services
.iter()
.map(|service| get_service(&containers, &name, service))
.collect();
stacks.push(StackInfo {
folder,
name,
active,
services,
@ -207,15 +210,13 @@ pub async fn remove(
Ok(())
}
pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> {
pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<String> {
// Check that it's a valid nix tree
rnix::Root::parse(source)
.ok()
.map_err(|err| AppError::Client {
status: StatusCode::NOT_ACCEPTABLE,
code: "failed-nix-parse",
message: format!("Parse error: {}", err),
})?;
let info = parse_arion_compose(source).map_err(|err| AppError::Client {
status: StatusCode::NOT_ACCEPTABLE,
code: "failed-nix-parse",
message: format!("Parse error: {}", err),
})?;
// Create a temporary stack and check that it generates a YAML tree
let dir = tempdir()?;
@ -239,7 +240,7 @@ pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> {
message: format!("Arion {}", err),
})
} else {
Ok(())
Ok(info.project)
}
}

View File

@ -5,6 +5,7 @@ use crate::{
},
node::{
container::ContainerInfo,
nix::parse_arion_compose,
stack::{
check_compose, command, commit_compose, create_new, get_compose, get_containers,
remove, write_compose, StackCommand,
@ -28,6 +29,7 @@ use serde_json::json;
#[derive(Template)]
#[template(path = "stack/get-one.html")]
struct GetOneTemplate {
stack_folder: String,
stack_name: String,
file_contents: String,
containers: Vec<ContainerInfo>,
@ -44,22 +46,20 @@ struct ConfirmDeleteTemplate {
}
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),
get_containers(&state.docker, &stack_name)
);
let file_contents = file_contents_res?;
let containers = containers_res?;
let file_contents = get_compose(&state.stack_dir, &stack_name).await?;
let info = parse_arion_compose(&file_contents)?;
let containers = get_containers(&state.docker, &info.project).await?;
reply(
json!({
"name": stack_name,
"folder": stack_name,
"name": info.project,
"file": file_contents,
"containers": containers,
}),
GetOneTemplate {
stack_name,
stack_folder: stack_name,
stack_name: info.project,
file_contents,
containers: containers.iter().map(|c| c.clone().into()).collect(),
},

View File

@ -7,7 +7,10 @@
<header>
<h1>Container <span class="container-name">{{container_name}}</span> {% match info.stack() %}{% when
Some
with (stack) %}(<a class="stack-name" href="/stack/{{stack}}/">{{stack}}</a>)
with (stack) %}({% match info.stack_folder() %}{% when
Some
with (stack_folder) %}<a class="stack-name" href="/stack/{{stack_folder}}/">{{stack}}</a>)
{% when None %}{% endmatch %}
{% when None %}{% endmatch %}</h1>
<div class="actions">
<form action="./start" method="POST"><button type="submit">Start</button></form>

View File

@ -87,7 +87,7 @@
<tr>
<td class="status {% if stack.active %}active{% endif %}"></td>
<td>
<a href="/stack/{{stack.name}}/">
<a href="/stack/{{stack.folder}}/">
{{stack.name}}
</a>
</td>

View File

@ -9,9 +9,6 @@
</header>
<section class="editor">
<form method="POST" id="editor-form">
<div class="row">
<input style="flex:1" name="name" type="text" placeholder="Stack name" required />
</div>
<div class="error"></div>
<textarea name="source" id="editor" required>{}</textarea>
<div class="row">