use std::io::Write;
use anyhow::{Context, Result};
use clap::{Arg, ArgMatches, Command};
use serde::{Deserialize, Serialize};
use termcolor::{Color, ColorChoice, StandardStream, WriteColor};
use crate::{
CliCommand,
constants::{ERROR_FAILED_TO_SEND_REQUEST, get_platform_management_api_url, get_platform_ui_url},
core::{
command::command,
http_client,
validate::{require_active_account, require_integration, require_manifest, resolve_auth},
},
deploy::utils::stream_deployment_status,
};
#[derive(Debug, Serialize)]
struct DestroyDeploymentRequest {
mode: String,
}
#[derive(Debug, Deserialize)]
struct DestroyDeploymentResponse {
#[allow(dead_code)]
id: String,
#[allow(dead_code)]
status: String,
}
#[derive(Debug)]
pub(crate) struct DestroyCommand;
impl DestroyCommand {
pub(crate) fn new() -> Self {
Self {}
}
}
impl CliCommand for DestroyCommand {
fn command(&self) -> Command {
command("destroy", "Destroy application infrastructure")
.arg(
Arg::new("environment")
.long("environment")
.short('e')
.required(true)
.help("Environment name (e.g., staging, production)"),
)
.arg(
Arg::new("region")
.long("region")
.required(true)
.help("AWS region (e.g., us-east-1)"),
)
.arg(
Arg::new("mode")
.long("mode")
.help("Destroy mode: 'all' (default) or 'preserve-data'")
.value_parser(["all", "preserve-data"]),
)
.arg(
Arg::new("base_path")
.long("path")
.short('p')
.help("Path to application root (optional)"),
)
.arg(
Arg::new("no-wait")
.long("no-wait")
.action(clap::ArgAction::SetTrue)
.help("Don't wait for destruction to complete"),
)
}
fn handler(&self, matches: &ArgMatches) -> Result<()> {
let mut stdout = StandardStream::stdout(ColorChoice::Always);
let auth_mode = resolve_auth()?;
require_active_account(&auth_mode)?;
let (_app_root, manifest) = require_manifest(matches)?;
let application_id = require_integration(&manifest)?;
let environment = matches
.get_one::<String>("environment")
.ok_or_else(|| anyhow::anyhow!("Environment is required"))?
.to_lowercase();
let region = matches
.get_one::<String>("region")
.ok_or_else(|| anyhow::anyhow!("Region is required"))?;
let mode = matches
.get_one::<String>("mode")
.cloned()
.unwrap_or_else(|| "all".to_string());
let wait = !matches.get_flag("no-wait");
log_header!(stdout, Color::Red, "DESTROYING INFRASTRUCTURE: {} ({}) [{}]",
environment, region, mode
);
writeln!(stdout)?;
let request_body = DestroyDeploymentRequest { mode: mode.clone() };
let url = format!(
"{}/applications/{}/environments/{}/regions/{}/destroy",
get_platform_management_api_url(),
application_id,
environment,
region
);
let response =
http_client::post_with_auth(&auth_mode, &url, serde_json::to_value(&request_body)?)
.with_context(|| ERROR_FAILED_TO_SEND_REQUEST)?;
let status = response.status();
if status.is_success() {
let response_text = response.text().with_context(|| "Failed to read response")?;
let deployment: DestroyDeploymentResponse = serde_json::from_str(&response_text)
.with_context(|| format!("Failed to parse destroy response: {}", response_text))?;
log_ok!(stdout, "Triggered destruction");
log_info!(stdout, "Deployment ID: {}", deployment.id);
let dashboard_url = format!(
"{}/dashboard/deployments/{}",
get_platform_ui_url(), deployment.id
);
if wait {
writeln!(stdout)?;
stream_deployment_status(
&auth_mode,
&deployment.id,
None,
None,
&mut stdout,
)?;
writeln!(stdout)?;
log_info!(stdout, "Dashboard: {}", dashboard_url);
} else {
writeln!(stdout)?;
writeln!(stdout, "Destruction started. Check status at:")?;
writeln!(stdout, " {}", dashboard_url)?;
}
} else if status.as_u16() == 409 {
let error_text = response
.text()
.unwrap_or_else(|_| "Unknown error".to_string());
log_error!(stdout, "Deployment conflict");
anyhow::bail!(
"Deployment conflict: {}. Wait for the current deployment to complete or cancel it first.",
error_text
);
} else if status.as_u16() == 403 {
let error_text = response
.text()
.unwrap_or_else(|_| "Unknown error".to_string());
log_error!(stdout, "Forbidden");
anyhow::bail!("{}", error_text);
} else {
let error_text = response
.text()
.unwrap_or_else(|_| "Unknown error".to_string());
log_error!(stdout, "Failed to destroy infrastructure (Status: {})", status);
anyhow::bail!(
"Failed to destroy infrastructure: {} (Status: {})",
error_text,
status
);
}
Ok(())
}
}