forklaunch 1.18.0

Launch faster with forklaunch
use std::io::Write;

use anyhow::{Context, Result};
use clap::{Arg, ArgAction, ArgMatches, Command};
use serde::{Deserialize, Serialize};
use serde_json::json;
use termcolor::{ColorChoice, StandardStream, WriteColor};

use crate::{
    CliCommand,
    core::command::command,
    managed::{
        client::{Missing, post_json, print_dryrun, require_managed_mode, resolve_managed_auth},
        types::ManagedInstance,
    },
};

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct Promotion {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    template_slug: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    version_semver: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    version_status: Option<String>,
    #[serde(default)]
    adopted: Vec<ManagedInstance>,
    #[serde(default)]
    skipped: Vec<Skipped>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
struct Skipped {
    environment: String,
    region: String,
    reason: String,
}

#[derive(Debug)]
pub(super) struct PromoteCommand;

impl PromoteCommand {
    pub(super) fn new() -> Self {
        Self
    }
}

impl CliCommand for PromoteCommand {
    fn command(&self) -> Command {
        command(
            "promote",
            "Make a running application a managed template; optionally adopt its deploys as instances",
        )
        .long_about(
            "Make a running application a managed template; optionally adopt its deploys.\n\n\
             The application's repository becomes the template's source and its current\n\
             release becomes version 1. Adoption is the default; the template and the\n\
             version are published at once and every deploy the application already has,\n\
             one per environment x region with a recorded host, becomes an ACTIVE managed\n\
             instance of it: same application, same hosts, same secrets, same data. Nothing\n\
             is redeployed. From then on rollouts, resize, apply-variables and key rotation\n\
             drive it like any instance, and new customers get fresh copies.\n\n\
             With --no-adopt the template stays a draft and version 1 is built through the\n\
             template pipeline; the application is left as it is.\n\n\
             Needs the admin role. 409 names the reason: PROMOTE_NO_REPOSITORY (connect a\n\
             git repository first), PROMOTE_NO_RELEASE (release it first), PROMOTE_SLUG_TAKEN,\n\
             PROMOTE_ALREADY_MANAGED.",
        )
        .arg(
            Arg::new("application")
                .long("application")
                .required(true)
                .help("Id of the application to promote"),
        )
        .arg(
            Arg::new("slug")
                .long("slug")
                .required(true)
                .help("Slug for the new template (lower-case, digits, dashes)"),
        )
        .arg(Arg::new("name").long("name").help("Template name (default: the application's)"))
        .arg(
            Arg::new("description")
                .long("description")
                .help("Template description (default: the application's)"),
        )
        .arg(
            Arg::new("no_adopt")
                .long("no-adopt")
                .help("Do not adopt the running deploys; build version 1 through the pipeline")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("dryrun")
                .long("dryrun")
                .help("Print the request that would be sent without sending it")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("json")
                .long("json")
                .help("Output raw JSON instead of formatted terminal output")
                .action(ArgAction::SetTrue),
        )
    }

    fn handler(&self, matches: &ArgMatches) -> Result<()> {
        let mut stdout = StandardStream::stdout(ColorChoice::Always);
        let application = matches
            .get_one::<String>("application")
            .context("--application is required")?;
        let slug = matches
            .get_one::<String>("slug")
            .context("--slug is required")?;
        let adopt = !matches.get_flag("no_adopt");

        let mut body = serde_json::Map::new();
        body.insert("applicationId".into(), json!(application));
        body.insert("slug".into(), json!(slug));
        body.insert("adopt".into(), json!(adopt));
        if let Some(name) = matches.get_one::<String>("name") {
            body.insert("name".into(), json!(name));
        }
        if let Some(description) = matches.get_one::<String>("description") {
            body.insert("description".into(), json!(description));
        }
        let body = serde_json::Value::Object(body);

        if matches.get_flag("dryrun") {
            return print_dryrun("POST", "/templates/from-application", Some(&body));
        }

        let auth_mode = resolve_managed_auth()?;
        require_managed_mode(&auth_mode)?;

        let out: Promotion = post_json(
            &auth_mode,
            "/templates/from-application",
            body,
            Missing::Resource(format!("application '{}'", application)),
        )?;

        if matches.get_flag("json") {
            println!("{}", serde_json::to_string_pretty(&out)?);
            return Ok(());
        }

        log_ok!(
            stdout,
            "'{}' is now a managed template (version {} {})",
            out.template_slug.as_deref().unwrap_or(slug),
            out.version_semver.as_deref().unwrap_or("?"),
            out.version_status.as_deref().unwrap_or("?")
        );
        if adopt {
            log_info!(
                stdout,
                "{} deploy(s) adopted as instance(s):",
                out.adopted.len()
            );
            for instance in &out.adopted {
                writeln!(
                    stdout,
                    "    {:<38} {:<12} {:<12} {}",
                    instance.id.as_deref().unwrap_or("-"),
                    instance.environment.as_deref().unwrap_or("production"),
                    instance.region.as_deref().unwrap_or("-"),
                    instance.host.as_deref().unwrap_or("-")
                )?;
            }
            for skipped in &out.skipped {
                log_info!(
                    stdout,
                    "skipped {}/{}: {}",
                    skipped.environment,
                    skipped.region,
                    skipped.reason
                );
            }
        } else {
            log_info!(
                stdout,
                "Version 1 is building through the template pipeline; publish the template once it is built (`template publish-template --slug {}`).",
                slug
            );
        }
        writeln!(stdout)?;
        Ok(())
    }
}