staxman-old/src/node/stack.rs

190 lines
5.1 KiB
Rust

use super::{
container::ContainerInfo, error::StackError, git::ThreadSafeRepository,
nix::parse_arion_compose,
};
use crate::http::error::Result;
use bollard::{container::ListContainersOptions, service::ContainerSummary, Docker};
use serde::Serialize;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use tokio::fs;
use xshell::Shell;
const COMPOSE_FILE: &str = "arion-compose.nix";
async fn is_stack(dir: &Path) -> Result<bool> {
Ok(fs::try_exists(dir.join(COMPOSE_FILE)).await?)
}
pub async fn get_containers(docker: &Docker, stack_name: &str) -> Result<Vec<ContainerSummary>> {
Ok(docker
.list_containers(Some(ListContainersOptions {
all: true,
limit: None,
size: true,
filters: HashMap::from([(
"label".to_string(),
vec![format!("com.docker.compose.project={}", stack_name)],
)]),
}))
.await?)
}
pub async fn get_compose(base_dir: &Path, stack_name: &str) -> Result<String> {
let dir = base_dir.join(stack_name);
if !is_stack(&dir).await? {
return Err(StackError::NotFound.into());
}
let contents = tokio::fs::read_to_string(dir.join(COMPOSE_FILE)).await?;
Ok(contents)
}
#[derive(Serialize)]
pub struct ServiceInfo {
name: String,
container: Option<String>,
running: bool,
}
#[derive(Serialize)]
pub struct StackInfo {
pub name: String,
pub active: bool,
pub services: Vec<ServiceInfo>,
}
impl StackInfo {
pub fn stats(&self) -> (usize, usize) {
let running = self.services.iter().filter(|s| s.running).count();
(running, self.services.len() - running)
}
}
#[derive(Serialize)]
pub struct NodeInfo {
pub stacks: Vec<StackInfo>,
pub containers: Vec<ContainerInfo>,
}
fn get_service(containers: &Vec<ContainerInfo>, stack_name: &str, service: &str) -> ServiceInfo {
let container = containers.iter().find(|cont| {
let labels = cont.labels.clone().unwrap_or_default();
labels.get("com.docker.compose.project") == Some(&stack_name.to_string())
&& labels.get("com.docker.compose.service") == Some(&service.to_string())
});
match container {
Some(info) => ServiceInfo {
name: service.to_string(),
container: Some(info.name.clone()),
running: info.running(),
},
_ => ServiceInfo {
name: service.to_string(),
container: None,
running: false,
},
}
}
pub async fn list(base_dir: &Path, docker: &Docker) -> Result<NodeInfo> {
let containers: Vec<ContainerInfo> = docker
.list_containers(Some(ListContainersOptions::<String> {
all: true,
limit: None,
..Default::default()
}))
.await?
.iter()
.map(|c| c.clone().into())
.collect();
let mut dirs = fs::read_dir(base_dir).await?;
let mut stacks = vec![];
while let Some(dir) = dirs.next_entry().await? {
let meta = dir.metadata().await?;
if !meta.is_dir() {
continue;
}
if is_stack(&dir.path()).await? {
let name = dir.file_name().to_string_lossy().to_string();
// 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 {
name,
active,
services,
})
}
}
Ok(NodeInfo { stacks, containers })
}
pub async fn write_compose(base_dir: &Path, stack_name: &str, contents: &str) -> Result<()> {
let dir = base_dir.join(stack_name);
if !is_stack(&dir).await? {
return Err(StackError::NotFound.into());
}
Ok(fs::write(dir.join(COMPOSE_FILE), contents).await?)
}
pub fn commit_compose(
repository: ThreadSafeRepository,
stack_name: &str,
message: &str,
) -> Result<()> {
let compose_path = format!("{}/{}", stack_name, COMPOSE_FILE);
repository.commit_file(&PathBuf::from(compose_path), message)?;
Ok(())
}
pub async fn command(
base_dir: &Path,
stack_name: &str,
arion_bin: &Path,
action: StackCommand,
) -> Result<()> {
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(())
}
pub enum StackCommand {
Down,
Start,
Stop,
Restart,
}
impl StackCommand {
fn command(&self) -> &[&str] {
match self {
StackCommand::Down => &["down"],
StackCommand::Start => &["up", "-d"],
StackCommand::Stop => &["stop"],
StackCommand::Restart => &["restart"],
}
}
}