use std::path::PathBuf;
use super::Project;
use super::deploy::Apps;
use crate::error::{Error, Result};
use crate::org::OrgId;
impl Apps {
fn projects_dir(&self, org: &OrgId) -> PathBuf {
self.apps_dir(org).join("projects")
}
pub fn project_create(
&self,
org: &OrgId,
name: &str,
description: &str,
environments: &[String],
) -> Result<Project> {
super::validate_part("project", name)?;
let envs: Vec<String> = if environments.is_empty() {
vec![super::DEFAULT_ENVIRONMENT.into()]
} else {
environments.to_vec()
};
for e in &envs {
super::validate_part("environment", e)?;
self.check_env_stack_free(org, name, e)?;
}
let _g = self.inner.edit.lock().unwrap();
let p = self.projects_dir(org).join(format!("{name}.json"));
if p.exists() {
return Err(Error::AlreadyExists(format!("project {name}")));
}
let mut envs = envs;
envs.dedup();
let proj = Project {
name: name.into(),
description: description.into(),
environments: envs,
created_at: crate::stack::now_secs(),
compose: Vec::new(),
};
self.save_project(org, &proj)?;
Ok(proj)
}
pub fn project_get(&self, org: &OrgId, name: &str) -> Result<Project> {
super::validate_part("project", name)?;
let p = self.projects_dir(org).join(format!("{name}.json"));
match std::fs::read(&p) {
Ok(b) => Ok(serde_json::from_slice(&b)?),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Err(Error::NotFound(format!("project {name} in org {org}")))
}
Err(e) => Err(e.into()),
}
}
pub fn project_list(&self, org: &OrgId) -> Result<Vec<Project>> {
let mut out = self.projects_raw(org)?;
for p in &mut out {
p.compose.retain(|r| self.stack_live(org, &r.stack));
}
Ok(out)
}
pub(super) fn projects_raw(&self, org: &OrgId) -> Result<Vec<Project>> {
let mut out = Vec::new();
let Ok(rd) = std::fs::read_dir(self.projects_dir(org)) else {
return Ok(out);
};
for e in rd.flatten() {
let p = e.path();
if p.extension().is_some_and(|x| x == "json") {
match serde_json::from_slice::<Project>(&std::fs::read(&p)?) {
Ok(pr) => out.push(pr),
Err(e) => eprintln!("isb serve: skipping {}: {e}", p.display()),
}
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
pub fn project_delete(&self, org: &OrgId, name: &str) -> Result<()> {
let _g = self.inner.edit.lock().unwrap();
self.project_get(org, name)?;
let apps: Vec<String> = self
.list(org)?
.into_iter()
.filter(|a| a.spec.project == name)
.map(|a| a.spec.name)
.collect();
if !apps.is_empty() {
return Err(Error::invalid(format!(
"project {name} still has apps: {}; delete them first",
apps.join(", ")
)));
}
let stacks = self.live_compose(org, name, None);
if !stacks.is_empty() {
return Err(Error::invalid(format!(
"project {name} still has compose stacks: {}; remove them first (isb stack rm)",
stacks.join(", ")
)));
}
std::fs::remove_file(self.projects_dir(org).join(format!("{name}.json")))?;
self.invalidate_owners(org);
Ok(())
}
fn check_env_stack_free(&self, org: &OrgId, project: &str, env: &str) -> Result<()> {
let s = super::stack_name(project, env)?;
if self.compose_stack_exists(org, &s) {
return Err(Error::invalid(format!(
"{project} + {env}: the environment would run as stack {s}, which is a compose stack; pick another name"
)));
}
Ok(())
}
pub fn environment_create(&self, org: &OrgId, project: &str, env: &str) -> Result<Project> {
super::validate_part("environment", env)?;
self.check_env_stack_free(org, project, env)?;
let _g = self.inner.edit.lock().unwrap();
let mut p = self.project_get(org, project)?;
if p.environments.iter().any(|e| e == env) {
return Err(Error::AlreadyExists(format!(
"environment {env} in project {project}"
)));
}
p.environments.push(env.into());
self.save_project(org, &p)?;
Ok(p)
}
pub fn environment_delete(&self, org: &OrgId, project: &str, env: &str) -> Result<Project> {
let _g = self.inner.edit.lock().unwrap();
let mut p = self.project_get(org, project)?;
if !p.environments.iter().any(|e| e == env) {
return Err(Error::NotFound(format!(
"environment {env} in project {project}"
)));
}
let apps: Vec<String> = self
.list(org)?
.into_iter()
.filter(|a| a.spec.project == project && a.spec.environment == env)
.map(|a| a.spec.name)
.collect();
if !apps.is_empty() {
return Err(Error::invalid(format!(
"environment {env} still has apps: {}; delete them first",
apps.join(", ")
)));
}
let stacks = self.live_compose(org, project, Some(env));
if !stacks.is_empty() {
return Err(Error::invalid(format!(
"environment {env} still has compose stacks: {}; remove them first (isb stack rm)",
stacks.join(", ")
)));
}
p.environments.retain(|e| e != env);
p.compose.retain(|r| r.environment != env);
self.save_project(org, &p)?;
Ok(p)
}
pub(super) fn save_project(&self, org: &OrgId, p: &Project) -> Result<()> {
super::write_atomic(
&self.projects_dir(org).join(format!("{}.json", p.name)),
&serde_json::to_vec_pretty(p)?,
)?;
self.invalidate_owners(org);
Ok(())
}
}