use serde::Deserialize;
use serde_json::{Value, json};
use super::super::{Daemon, args, obj};
use super::org_of;
use crate::error::{Error, Result};
use crate::org::OrgId;
use crate::server::{Caller, Registry, Tool};
pub(in crate::daemon) fn delete_tools(r: &mut Registry, d: &std::sync::Arc<Daemon>) -> Result<()> {
let destructive = json!({"destructiveHint": true, "openWorldHint": false});
let force = json!({"type": "boolean", "description": "Delete its apps and compose stacks too (default false)."});
let volumes = json!({"type": "boolean", "description": "With force: delete their named volumes too, the data in them for good (default false: kept). A volume another stack still uses is kept. Org admins and owners."});
let dry_run = json!({"type": "boolean", "description": "Only report what force (and volumes) would delete."});
tool!(
r,
d,
"project_delete",
"Delete a project",
"Delete a project. Refused while it has apps or compose stacks, unless force=true: then every app in it is deleted (as app_delete) and every compose stack removed (as stack_remove), in every environment, before the project goes. Their named volumes are kept unless volumes=true. Returns what was deleted; dry_run=true only says what would be.",
obj(
json!({"name": {"type": "string"}, "force": force, "volumes": volumes, "dry_run": dry_run}),
&["name"]
),
destructive,
|d: &Daemon, a: Value, c: &Caller| -> Result<Value> {
let org = org_of(&a)?;
let a: DeleteArgs = args(a)?;
let name = a
.name
.clone()
.ok_or_else(|| Error::invalid("name is required"))?;
d.apps.project_get(&org, &name)?;
let mut out = empty(d, c, &org, &name, None, &a)?;
if !a.dry_run {
d.apps.project_delete(&org, &name)?;
}
out["ok"] = json!(true);
Ok(out)
}
);
tool!(
r,
d,
"environment_delete",
"Delete an environment",
"Remove an environment from a project. Refused while it has apps or compose stacks, unless force=true: then its apps are deleted (as app_delete) and its compose stacks removed (as stack_remove) first. Their named volumes are kept unless volumes=true. Returns the project and what was deleted; dry_run=true only says what would be.",
obj(
json!({"project": {"type": "string"}, "name": {"type": "string"}, "force": force, "volumes": volumes, "dry_run": dry_run}),
&["project", "name"]
),
destructive,
|d: &Daemon, a: Value, c: &Caller| -> Result<Value> {
let org = org_of(&a)?;
let a: DeleteArgs = args(a)?;
let project = a
.project
.clone()
.ok_or_else(|| Error::invalid("project is required"))?;
let name = a
.name
.clone()
.ok_or_else(|| Error::invalid("name is required"))?;
let p = d.apps.project_get(&org, &project)?;
if !p.environments.contains(&name) {
return Err(Error::NotFound(format!(
"environment {name} in project {project}"
)));
}
let done = empty(d, c, &org, &project, Some(&name), &a)?;
let mut out = if a.dry_run {
json!(p)
} else {
json!(d.apps.environment_delete(&org, &project, &name)?)
};
for (k, v) in done.as_object().into_iter().flatten() {
out[k] = v.clone();
}
Ok(out)
}
);
Ok(())
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct DeleteArgs {
#[serde(default)]
project: Option<String>,
#[serde(default)]
name: Option<String>,
#[serde(default)]
force: bool,
#[serde(default)]
volumes: bool,
#[serde(default)]
dry_run: bool,
#[serde(default)]
#[allow(dead_code)]
org: Option<String>,
}
fn empty(
d: &Daemon,
c: &Caller,
org: &OrgId,
project: &str,
env: Option<&str>,
a: &DeleteArgs,
) -> Result<Value> {
if a.volumes && !a.force {
return Err(Error::invalid("volumes needs force"));
}
if a.volumes && !a.dry_run {
super::super::volumes::require_admin(c, org, "deleting volumes")?;
}
let (apps, stacks) = if a.force || a.dry_run {
d.apps.contents(org, project, env)?
} else {
Default::default()
};
let p = d.apps.project_get(org, project)?;
let mut ours: Vec<String> = p
.environments
.iter()
.filter(|e| env.is_none_or(|x| x == e.as_str()))
.filter_map(|e| crate::app::stack_name(project, e).ok())
.collect();
ours.extend(stacks.iter().cloned());
let defs = d.ctl.definitions();
let mut volumes: Vec<String> = Vec::new();
let mut shared: Vec<String> = Vec::new();
for def in defs.iter().filter(|s| s.org == *org) {
for v in named_volumes(def) {
let list = if ours.contains(&def.name) {
&mut volumes
} else {
&mut shared
};
if !list.contains(&v) {
list.push(v);
}
}
}
let (keep, volumes): (Vec<String>, Vec<String>) =
volumes.into_iter().partition(|v| shared.contains(v));
let mut kept: Vec<Value> = keep
.iter()
.map(|v| json!({"name": v, "reason": "another stack uses it"}))
.collect();
if a.dry_run {
return Ok(json!({
"dry_run": true, "apps": apps, "stacks": stacks, "volumes": volumes, "volumes_kept": kept
}));
}
for app in &apps {
d.apps.delete(org, app)?;
}
for s in &stacks {
super::super::tools::remove_stack(d, &crate::stack::qualified(org, s), false)?;
}
let mut deleted = Vec::new();
if a.volumes {
for v in volumes {
match d.volumes.delete(org, &v) {
Ok(()) => deleted.push(v),
Err(e) if e.is_not_found() => {}
Err(e) => kept.push(json!({"name": v, "reason": e.to_string()})),
}
}
}
Ok(json!({
"apps_deleted": apps, "stacks_removed": stacks, "volumes_deleted": deleted, "volumes_kept": kept
}))
}
pub(in crate::daemon) fn empty_org(d: &Daemon, org: &OrgId, notes: &mut Vec<String>) -> Result<()> {
for a in d.apps.list(org)? {
notes.push(format!("{org}: deleting app {}", a.spec.name));
d.apps.delete(org, &a.spec.name)?;
}
for def in d.ctl.definitions().iter().filter(|s| s.org == *org) {
notes.push(format!("{org}: removing stack {}", def.name));
super::super::tools::remove_stack(d, &def.qualified(), false)?;
}
for p in d.apps.project_list(org)? {
d.apps.project_delete(org, &p.name)?;
}
Ok(())
}
fn named_volumes(def: &crate::stack::StackDef) -> Vec<String> {
let mut out = Vec::new();
for spec in def.file.services.values() {
for v in &spec.volumes {
if v.mount_type != crate::spec::MountType::Volume || v.external {
continue;
}
let top = def.file.volumes.get(&v.source);
if top.is_some_and(|t| t.external) {
continue;
}
let name = top
.and_then(|t| t.name.clone())
.unwrap_or_else(|| v.source.clone());
if !out.contains(&name) {
out.push(name);
}
}
}
out
}