190 lines
5.1 KiB
Rust
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"],
|
|
}
|
|
}
|
|
}
|