zilliz 1.4.2

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
use anyhow::{bail, Result};

use crate::api::client::ApiClient;
use crate::cli::args::ConfigureCommands;
use crate::config::manager::ConfigManager;
use crate::model::loader::ModelLoader;

const CREDENTIAL_KEYS: &[&str] = &["api_key"];

/// Entry point: dispatch to interactive flow or subcommand.
pub async fn run(config_mgr: &ConfigManager, subcmd: Option<ConfigureCommands>) -> Result<()> {
    match subcmd {
        None => run_interactive(config_mgr).await,
        Some(ConfigureCommands::Set { key, value }) => run_set(config_mgr, &key, value).await,
        Some(ConfigureCommands::Get { key }) => run_get(config_mgr, &key),
        Some(ConfigureCommands::List) => run_list(config_mgr),
        Some(ConfigureCommands::Clear) => run_clear(config_mgr),
    }
}

/// Interactive configure flow (bare `zilliz configure`).
async fn run_interactive(config_mgr: &ConfigManager) -> Result<()> {
    let api_key =
        rpassword::prompt_password("Zilliz Cloud API Key: ").map(|s| s.trim().to_string())?;

    validate_ascii(&api_key, "API key")?;

    if api_key.is_empty() {
        bail!("API key cannot be empty.");
    }

    validate_api_key(config_mgr, &api_key).await?;
    config_mgr.set_credential("api_key", &api_key)?;
    println!("API Key configured successfully.");

    Ok(())
}

/// `configure set <key> [value]`
async fn run_set(config_mgr: &ConfigManager, key: &str, value: Option<String>) -> Result<()> {
    let is_credential = CREDENTIAL_KEYS.contains(&key);

    let value = match value {
        Some(v) => v,
        None => {
            if is_credential {
                rpassword::prompt_password(format!("Enter {}: ", key))
                    .map(|s| s.trim().to_string())?
            } else {
                bail!("VALUE is required for non-credential keys.");
            }
        }
    };

    if is_credential {
        validate_ascii(&value, key)?;
        if value.is_empty() {
            bail!("{} cannot be empty.", key);
        }
        if key == "api_key" {
            validate_api_key(config_mgr, &value).await?;
        }
        config_mgr.set_credential(key, &value)?;
    } else {
        config_mgr.set_config("default", key, &value)?;
    }

    let display = if is_credential {
        mask_value(&value)
    } else {
        value
    };
    println!("{} = {}", key, display);

    Ok(())
}

/// `configure get <key>`
fn run_get(config_mgr: &ConfigManager, key: &str) -> Result<()> {
    let is_credential = CREDENTIAL_KEYS.contains(&key);

    let value = if is_credential {
        config_mgr.get_credential(key)
    } else {
        config_mgr.get_config("default", key)
    };

    match value {
        Some(v) => {
            let display = if is_credential { mask_value(&v) } else { v };
            println!("{} = {}", key, display);
        }
        None => {
            println!("{}: not set", key);
        }
    }

    Ok(())
}

/// `configure list`
fn run_list(config_mgr: &ConfigManager) -> Result<()> {
    let (credentials, config) = config_mgr.list_all();

    for (key, value) in &credentials {
        println!("{} = {}", key, mask_value(value));
    }
    for (key, value) in &config {
        println!("{} = {}", key, value);
    }

    Ok(())
}

/// `configure clear`
fn run_clear(config_mgr: &ConfigManager) -> Result<()> {
    config_mgr.clear_credentials()?;
    println!("Credentials cleared.");
    Ok(())
}

/// Validate an API key by calling the clusters endpoint.
async fn validate_api_key(config_mgr: &ConfigManager, api_key: &str) -> Result<()> {
    println!("Validating API key...");

    let models = ModelLoader::load_builtin()?;
    let base_url =
        super::endpoint::resolve_control_plane_url(config_mgr, &models.control_plane, None);

    let list_path = models
        .control_plane
        .resources
        .get("cluster")
        .and_then(|r| r.operations.get("list"))
        .map(|op| op.path().to_string())
        .unwrap_or_else(|| "/v2/clusters".to_string());

    let client = ApiClient::new(api_key.to_string(), base_url);

    client.call("GET", &list_path, None, None).await?;

    Ok(())
}

/// Reject non-ASCII input.
fn validate_ascii(value: &str, name: &str) -> Result<()> {
    if !value.is_ascii() {
        bail!(
            "{} contains non-ASCII characters. \
             Please check your input (on macOS, use Cmd+V to paste, not Option+V).",
            name
        );
    }
    Ok(())
}

/// Mask a value for display: first4 + **** + last4 for values >= 16 chars.
fn mask_value(value: &str) -> String {
    if value.len() >= 16 {
        format!("{}****{}", &value[..4], &value[value.len() - 4..])
    } else {
        "****".to_string()
    }
}