use anyhow::{Context, Result};
use tracing::{info, warn};
use crate::clever::Clever;
use crate::cli::DeleteArgs;
use crate::commands::OrgCache;
use crate::model::Project;
use crate::state::{ResourceKind, State};
pub fn run(args: DeleteArgs) -> Result<()> {
let mut variables: Vec<(String, String)> = Vec::new();
for path in &args.variable_paths {
variables.extend(
crate::model::load_variables_file(path)
.with_context(|| format!("loading --variable-path `{}`", path.display()))?,
);
}
variables.extend(args.variables);
if let Some(env) = args.env {
variables.push(("env".to_string(), env));
}
let (project, _resolver) = Project::load_and_resolve(
&args.file,
args.org,
args.region,
&variables,
args.secrets_path.as_deref(),
)
.with_context(|| format!("loading project `{}`", args.file.display()))?;
let clever = Clever::new()?.with_dry_run(args.dry_run);
if clever.is_dry_run() {
info!("[dry-run] no mutations will be sent to Clever Cloud");
}
let mut state = State::load(&args.file)?;
let mut cache = OrgCache::new();
let mut failures = 0usize;
for (key, app) in &project.apps {
if let Err(e) = delete_resource(
&clever,
&mut state,
&mut cache,
&project,
key,
&app.name,
ResourceKind::App,
) {
warn!("failed to delete app `{}`: {e:#} — continuing", app.name);
failures += 1;
}
}
for (key, addon) in &project.addons {
if let Err(e) = delete_resource(
&clever,
&mut state,
&mut cache,
&project,
key,
&addon.name,
ResourceKind::Addon,
) {
warn!(
"failed to delete addon `{}`: {e:#} — continuing",
addon.name
);
failures += 1;
}
}
if !clever.is_dry_run() {
state
.save()
.with_context(|| format!("saving state file `{}`", state.path().display()))?;
}
if failures > 0 {
warn!("delete finished with {failures} failure(s); see warnings above");
} else {
info!("delete complete");
}
Ok(())
}
fn delete_resource(
clever: &Clever,
state: &mut State,
cache: &mut OrgCache,
project: &Project,
key: &str,
name: &str,
kind: ResourceKind,
) -> Result<()> {
let kind_label = match kind {
ResourceKind::App => "app",
ResourceKind::Addon => "addon",
};
if let Some(r) = state.find(kind, name, &project.org) {
let id = r.id.clone();
info!("deleting {kind_label} `{name}` ({id}) [project key: {key}, from state]");
match call_delete(clever, kind, &id, &project.org) {
Ok(()) => {
if !clever.is_dry_run() {
state.remove_by_id(&id);
}
return Ok(());
}
Err(e) => {
warn!(
"delete via state-known id `{id}` failed: {e:#} — dropping stale entry and refreshing from clever"
);
state.remove_by_id(&id);
cache.invalidate();
}
}
}
let fresh_id = match kind {
ResourceKind::App => cache
.apps(clever, &project.org)?
.get(name)
.map(|a| a.app_id.clone()),
ResourceKind::Addon => cache
.addons(clever, &project.org)?
.get(name)
.map(|a| a.addon_id.clone()),
};
match fresh_id {
Some(id) => {
info!("deleting {kind_label} `{name}` ({id}) [project key: {key}, from listing]");
call_delete(clever, kind, &id, &project.org)?;
if !clever.is_dry_run() {
state.remove_by_id(&id);
}
Ok(())
}
None => {
warn!(
"{kind_label} `{name}` not found in state or org `{}` — skipping",
project.org
);
Ok(())
}
}
}
fn call_delete(clever: &Clever, kind: ResourceKind, id: &str, org: &str) -> Result<()> {
match kind {
ResourceKind::App => clever.delete_app(id),
ResourceKind::Addon => clever.delete_addon(id, org),
}
}