some naive handling of project renaming
This commit is contained in:
parent
f4915daa0c
commit
e3fecbee95
7 changed files with 86 additions and 28 deletions
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,11 +210,9 @@ 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 {
|
||||
let info = parse_arion_compose(source).map_err(|err| AppError::Client {
|
||||
status: StatusCode::NOT_ACCEPTABLE,
|
||||
code: "failed-nix-parse",
|
||||
message: format!("Parse error: {}", err),
|
||||
|
@ -239,7 +240,7 @@ pub async fn check_compose(arion_bin: &Path, source: &str) -> Result<()> {
|
|||
message: format!("Arion {}", err),
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
Ok(info.project)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Reference in a new issue