eggress-cli 1.0.7

CLI binary for the eggress multi-protocol proxy
Documentation
//! `eggress pproxy translate|check|run`: migration and compatibility tools.
//!
//! `translate` and `check` are migration/developer tools over the parsed
//! compatibility IR. `run` delegates to the shared
//! [`eggress_cli::pproxy_exec`] pipeline so the nested command and the
//! standalone `pproxy` binary cannot diverge.

use eggress_cli::{
    pproxy_exec::{self, PreparedAction},
    EXIT_CLI_PARSE_ERROR, EXIT_CONFIG_VALIDATION, EXIT_RUNTIME_FAILURE, EXIT_SUCCESS,
    EXIT_UNSUPPORTED_FEATURE,
};

use crate::cli::{PproxyAction, PproxyCheck, PproxyCommand, PproxyRun, PproxyTranslate};

/// Dispatch `eggress pproxy <action>`. Returns the process exit code.
pub fn handle_pproxy_command(cmd: &PproxyCommand) -> i32 {
    match &cmd.action {
        PproxyAction::Translate(args) => handle_pproxy_translate(args),
        PproxyAction::Check(args) => handle_pproxy_check(args),
        PproxyAction::Run(args) => handle_pproxy_run(args),
    }
}

fn handle_pproxy_translate(args: &PproxyTranslate) -> i32 {
    let pproxy_args = match eggress_pproxy_compat::PproxyArgs::parse(&args.args) {
        Ok(a) => a,
        Err(e) => {
            eprintln!("error: {e}");
            return EXIT_CLI_PARSE_ERROR;
        }
    };

    let output = match eggress_pproxy_compat::translate_pproxy_args(&pproxy_args) {
        Ok(o) => o,
        Err(e) => {
            eprintln!("error: {e}");
            return EXIT_CONFIG_VALIDATION;
        }
    };

    if output.has_unsupported() {
        for u in &output.unsupported() {
            eprintln!("warning: {u}");
        }
        eprintln!("\nGenerated TOML may be incomplete due to unsupported features.");
    }

    for w in &output.warnings() {
        eprintln!("warning: {w}");
    }

    if args.annotate {
        println!("# Generated by eggress pproxy translate");
        println!("# pproxy arguments: {}", args.args.join(" "));
        if !output.warnings().is_empty() || !output.unsupported().is_empty() {
            println!("#");
            for w in &output.warnings() {
                println!("# {w}");
            }
            for u in &output.unsupported() {
                println!("# {u}");
            }
        }
        println!();
    }

    print!("{}", output.toml);

    if output.has_unsupported() {
        return EXIT_UNSUPPORTED_FEATURE;
    }
    EXIT_SUCCESS
}

fn handle_pproxy_check(args: &PproxyCheck) -> i32 {
    let pproxy_args = match eggress_pproxy_compat::PproxyArgs::parse(&args.args) {
        Ok(a) => a,
        Err(e) => {
            eprintln!("error: {e}");
            return EXIT_CLI_PARSE_ERROR;
        }
    };

    let output = match eggress_pproxy_compat::translate_pproxy_args(&pproxy_args) {
        Ok(o) => o,
        Err(e) => {
            eprintln!("error: {e}");
            return EXIT_CONFIG_VALIDATION;
        }
    };

    let local_uris = pproxy_args.parse_local_uris();
    let remote_chains = pproxy_args.parse_remote_chains();

    if args.json {
        let listeners = match &local_uris {
            Ok(uris) => uris
                .iter()
                .map(|u| u.redacted_display().to_string())
                .collect(),
            Err(e) => vec![format!("error: {e}")],
        };
        let remotes = match &remote_chains {
            Ok(chains) => chains.iter().map(|c| c.redacted_display()).collect(),
            Err(e) => vec![format!("error: {e}")],
        };
        let remote_chains_info: Vec<ChainInfo> = match &remote_chains {
            Ok(chains) => chains
                .iter()
                .map(|c| ChainInfo {
                    raw: c.raw.clone(),
                    hops: c.hops.len(),
                    hop_schemes: c.hops.iter().map(|h| h.scheme.clone()).collect(),
                    is_chain: c.hops.len() > 1,
                })
                .collect(),
            Err(e) => vec![ChainInfo {
                raw: format!("error: {e}"),
                hops: 0,
                hop_schemes: vec![],
                is_chain: false,
            }],
        };

        let mut diagnostics: Vec<eggress_pproxy_compat::StructuredDiagnostic> = Vec::new();
        let mut features: Vec<FeatureInfo> = Vec::new();

        for w in &output.warnings() {
            let feature_tier = eggress_pproxy_compat::manifest_tier_for_category(w.category);
            diagnostics.push(eggress_pproxy_compat::StructuredDiagnostic::from(w));
            features.push(FeatureInfo {
                name: w.category.to_string(),
                tier: feature_tier.as_str().to_string(),
                diagnostic_code: Some(w.diagnostic_code()),
            });
        }
        for u in &output.unsupported() {
            let diag = eggress_pproxy_compat::StructuredDiagnostic {
                code: eggress_pproxy_compat::DiagnosticCode::UnsupportedProtocol,
                feature_id: Some(u.feature.to_string()),
                tier: Some(
                    eggress_pproxy_compat::ManifestTier::Unsupported
                        .as_str()
                        .to_string(),
                ),
                message: u.detail.clone(),
                suggestion: None,
            };
            diagnostics.push(diag);
            features.push(FeatureInfo {
                name: u.feature.to_string(),
                tier: eggress_pproxy_compat::ManifestTier::Unsupported
                    .as_str()
                    .to_string(),
                diagnostic_code: Some(eggress_pproxy_compat::DiagnosticCode::UnsupportedProtocol),
            });
        }

        let tier = eggress_pproxy_compat::classify_aggregate_tier(
            &output.warnings(),
            &output.unsupported(),
        );

        let check_output = PproxyCheckOutput {
            tier: tier.as_str().to_string(),
            diagnostics,
            features,
            raw_args: args.args.clone(),
            parsed_uris: ParsedUris {
                listeners,
                remotes,
                chain_info: remote_chains_info,
            },
        };

        match serde_json::to_string_pretty(&check_output) {
            Ok(json) => println!("{json}"),
            Err(e) => {
                eprintln!("failed to serialize check output: {e}");
                return EXIT_RUNTIME_FAILURE;
            }
        }
    } else {
        println!("pproxy compatibility check");
        println!("=========================");

        match local_uris {
            Ok(uris) => {
                for uri in &uris {
                    println!(
                        "  local:  {} -> scheme={}",
                        uri.redacted_display(),
                        uri.scheme
                    );
                }
            }
            Err(e) => eprintln!("  local:  error: {e}"),
        }

        match remote_chains {
            Ok(chains) => {
                for chain in &chains {
                    if chain.hops.len() > 1 {
                        println!(
                            "  remote: {} -> chain ({} hops: {})",
                            chain.redacted_display(),
                            chain.hops.len(),
                            chain
                                .hops
                                .iter()
                                .map(|h| h.scheme.as_str())
                                .collect::<Vec<_>>()
                                .join(" -> ")
                        );
                    } else if let Some(hop) = chain.hops.first() {
                        println!(
                            "  remote: {} -> scheme={}",
                            hop.redacted_display(),
                            hop.scheme
                        );
                    }
                }
            }
            Err(e) => eprintln!("  remote: error: {e}"),
        }

        let tier = eggress_pproxy_compat::classify_aggregate_tier(
            &output.warnings(),
            &output.unsupported(),
        );
        println!("\nparity tier: {}", tier_label(&tier));

        if !output.warnings().is_empty() {
            println!("\nwarnings:");
            for w in &output.warnings() {
                println!("  {w}");
            }
        }

        if !output.unsupported().is_empty() {
            println!("\nunsupported:");
            for u in &output.unsupported() {
                println!("  {u}");
            }
        }
    }
    EXIT_SUCCESS
}

fn handle_pproxy_run(args: &PproxyRun) -> i32 {
    match pproxy_exec::prepare(&args.args) {
        Ok(PreparedAction::PrintVersion) => {
            println!("{}", eggress_pproxy_compat::PproxyArgs::version_string());
            EXIT_SUCCESS
        }
        Ok(PreparedAction::PrintHelp) => {
            println!("pproxy compatibility binary (eggress-pproxy-compat)");
            println!("Use -l and -r with pproxy-compatible URIs.");
            println!("Run standalone `pproxy --help` for the complete option reference.");
            EXIT_SUCCESS
        }
        Ok(PreparedAction::Run(gated)) => {
            let prepared = match pproxy_exec::compile(*gated) {
                Ok(prepared) => prepared,
                Err(failure) => {
                    eprintln!("error: {}", failure.message);
                    return failure.code;
                }
            };
            // Compatibility verbosity/debug defaults come from the parsed
            // pproxy flags. An explicit RUST_LOG remains authoritative.
            crate::logging::init_pproxy_logging(&prepared.args, args.log_format);
            pproxy_exec::execute(prepared, "")
        }
        Err(failure) => {
            eprintln!("error: {}", failure.message);
            failure.code
        }
    }
}

#[derive(serde::Serialize)]
struct PproxyCheckOutput {
    tier: String,
    diagnostics: Vec<eggress_pproxy_compat::StructuredDiagnostic>,
    features: Vec<FeatureInfo>,
    raw_args: Vec<String>,
    parsed_uris: ParsedUris,
}

#[derive(serde::Serialize)]
struct FeatureInfo {
    name: String,
    tier: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    diagnostic_code: Option<eggress_pproxy_compat::DiagnosticCode>,
}

#[derive(serde::Serialize)]
struct ParsedUris {
    listeners: Vec<String>,
    remotes: Vec<String>,
    chain_info: Vec<ChainInfo>,
}

#[derive(serde::Serialize)]
struct ChainInfo {
    raw: String,
    hops: usize,
    hop_schemes: Vec<String>,
    is_chain: bool,
}

fn tier_label(tier: &eggress_pproxy_compat::ManifestTier) -> &'static str {
    match tier {
        eggress_pproxy_compat::ManifestTier::DropIn => "Drop-in",
        eggress_pproxy_compat::ManifestTier::CompatibleWithWarning => "Compatible (with warnings)",
        eggress_pproxy_compat::ManifestTier::NativeEquivalent => "Native equivalent",
        eggress_pproxy_compat::ManifestTier::IntentionalNonParity => "Intentional non-parity",
        eggress_pproxy_compat::ManifestTier::Unsupported => "Unsupported",
    }
}