cufflink-cli 0.7.15

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use crate::config::CliConfig;
use std::process::Command;

pub async fn run(
    target_version: Option<i32>,
    allow_destructive: bool,
    env: Option<&str>,
) -> eyre::Result<()> {
    let config = CliConfig::load_with_env(env)?;

    if let Some(ref name) = config.env_name {
        println!("Environment: {}", name);
    }

    // Get service name from manifest
    let output = Command::new("cargo")
        .args(["run", "--", "--emit-manifest"])
        .output()?;

    if !output.status.success() {
        eyre::bail!("Failed to build service. Run from a cufflink service directory.");
    }

    let stdout = String::from_utf8(output.stdout)?;
    let manifest: serde_json::Value = serde_json::from_str(stdout.trim())?;
    let service_name = manifest["name"].as_str().unwrap_or("unknown");

    // Find the service ID
    let client = config.http_client();
    let resp = config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services", config.api_url),
        )
        .send()
        .await?;

    let body: serde_json::Value = resp.json().await?;
    let services = body["services"]
        .as_array()
        .ok_or_else(|| eyre::eyre!("No services found"))?;

    let service = services
        .iter()
        .find(|s| s["name"].as_str() == Some(service_name))
        .ok_or_else(|| eyre::eyre!("Service '{}' not found. Deploy it first.", service_name))?;

    let service_id = service["id"]
        .as_str()
        .ok_or_else(|| eyre::eyre!("Service has no ID"))?;

    let current_version = service["current_version"].as_i64().unwrap_or(0);

    if current_version <= 1 && target_version.is_none() {
        eyre::bail!(
            "Cannot rollback: only one version exists (v{})",
            current_version
        );
    }

    let target = target_version.unwrap_or((current_version - 1) as i32);
    println!(
        "Rolling back {} from v{} to v{}...",
        service_name, current_version, target
    );

    let rollback_req = serde_json::json!({
        "target_version": target,
        "allow_destructive": allow_destructive,
    });

    let resp = config
        .auth_request(
            &client,
            reqwest::Method::POST,
            &format!("{}/api/services/{}/rollback", config.api_url, service_id),
        )
        .json(&rollback_req)
        .send()
        .await?;

    if resp.status().is_success() {
        let body: serde_json::Value = resp.json().await?;
        println!(
            "Rolled back {} to v{} (now v{})",
            body["name"].as_str().unwrap_or("?"),
            body["rolled_back_to"],
            body["new_version"]
        );
        println!("  Schema changes: {}", body["schema_changes"]);
        println!("  Deployment ID:  {}", body["deployment_id"]);
        if body["wasm_restored"].as_bool() == Some(true) {
            println!("  WASM binary:    restored from v{}", target);
        }
        if let Some(commit) = body["rolled_back_from_commit"].as_str() {
            println!("  Source commit:  {}", commit);
        }
    } else {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();

        if body.contains("Destructive changes detected") {
            eprintln!("Rollback blocked: {}", body);
            eprintln!();
            eprintln!("Re-run with --allow-destructive to force:");
            eprintln!("  cufflink rollback --allow-destructive");
            std::process::exit(1);
        }

        eyre::bail!("Rollback failed ({}): {}", status, body);
    }

    Ok(())
}