zilliz 1.4.2

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
//! Hand-written `zilliz external-collection refresh <action>` command.
//!
//! Three-level structure:
//!   external-collection refresh trigger  --name <name> [--database <db>] [--external-source <src>] [--external-spec <spec>]
//!   external-collection refresh describe --job-id <id>
//!   external-collection refresh list     [--name <name>] [--database <db>]
//!
//! HTTP endpoints (data-plane, requires kite-coordinator >= PR #5735):
//!   POST /v2/vectordb/jobs/external_collection/refresh
//!   POST /v2/vectordb/jobs/external_collection/describe
//!   POST /v2/vectordb/jobs/external_collection/list

use anyhow::{bail, Context, Result};
use serde_json::{json, Value};

use crate::api::client::ApiClient;
use crate::api::error::ApiError;
use crate::config::credentials::resolve_api_key;
use crate::config::manager::ConfigManager;
use crate::model::loader::Models;

use super::dispatch::{print_output_with_opts, OutputOpts};

const RESOURCE_DESC: &str = "Manage external collections (refresh jobs).";
const REFRESH_DESC: &str = "Manage refresh jobs (trigger / describe / list).";

/// Entry point dispatched from `lib.rs` for the `external-collection` resource.
///
/// `raw_args` layout: `[<operation>, <action>?, ...flags]` (the leading
/// `external-collection` is stripped by the dispatcher). The only operation
/// is `refresh`; actions are `trigger`, `describe`, `list`.
pub async fn run_from_args(
    _models: &Models,
    config_mgr: &ConfigManager,
    raw_args: &[String],
    output_opts: &OutputOpts<'_>,
) -> Result<()> {
    let op = raw_args.first().map(|s| s.as_str()).unwrap_or("");
    if op.is_empty() || op == "-h" || op == "--help" {
        print_resource_help();
        return Ok(());
    }
    if op != "refresh" {
        bail!(
            "Unknown external-collection operation '{}'. Available: refresh",
            op
        );
    }

    let action = raw_args.get(1).map(|s| s.as_str()).unwrap_or("");
    if action.is_empty() || action == "-h" || action == "--help" {
        print_refresh_help();
        return Ok(());
    }

    let rest: Vec<String> = if raw_args.len() > 2 {
        raw_args[2..].to_vec()
    } else {
        vec![]
    };

    match action {
        "trigger" => trigger(config_mgr, &rest, output_opts).await,
        "describe" => describe(config_mgr, &rest, output_opts).await,
        "list" => list(config_mgr, &rest, output_opts).await,
        other => bail!(
            "Unknown refresh action '{}'. Available: trigger, describe, list",
            other
        ),
    }
}

fn print_resource_help() {
    print!(
        "{}\n\n\
         Usage: zilliz external-collection <OPERATION> [OPTIONS]\n\n\
         Operations:\n\
         \x20 {:21}{}\n",
        RESOURCE_DESC, "refresh", REFRESH_DESC,
    );
}

fn print_refresh_help() {
    print!(
        "{}\n\n\
         Usage: zilliz external-collection refresh <ACTION> [OPTIONS]\n\n\
         Actions:\n\
         \x20 {:11}Trigger a refresh job for an external collection. Returns the jobId.\n\
         \x20 {:11}Get the status of a single refresh job.\n\
         \x20 {:11}List refresh jobs (optionally filtered by collection).\n",
        REFRESH_DESC, "trigger", "describe", "list",
    );
}

fn print_trigger_help() {
    print!(
        "Trigger a refresh job for an external collection. Returns the jobId.\n\n\
         Usage: zilliz external-collection refresh trigger [OPTIONS]\n\n\
         Options:\n\
         \x20 {:24}{:18}External collection name (required)\n\
         \x20 {:24}{:18}Database name\n\
         \x20 {:24}{:18}Override external source (optional)\n\
         \x20 {:24}{:18}Override external spec (optional)\n\n\
         Examples:\n\
         \x20 zilliz external-collection refresh trigger --name my_external_coll\n\
         \x20 zilliz external-collection refresh trigger --name my_external_coll --database my_db\n",
        "--name", "<string>", "--database", "<string>", "--external-source", "<string>",
        "--external-spec", "<string>",
    );
}

fn print_describe_help() {
    print!(
        "Get the status of a single external-collection refresh job.\n\n\
         Usage: zilliz external-collection refresh describe [OPTIONS]\n\n\
         Options:\n\
         \x20 {:24}{:18}Refresh job id, returned by trigger (required)\n\n\
         Examples:\n\
         \x20 zilliz external-collection refresh describe --job-id 123456\n",
        "--job-id", "<integer>",
    );
}

fn print_list_help() {
    print!(
        "List external-collection refresh jobs (optionally filtered by collection).\n\n\
         Usage: zilliz external-collection refresh list [OPTIONS]\n\n\
         Options:\n\
         \x20 {:24}{:18}Filter by external collection name\n\
         \x20 {:24}{:18}Database name\n\n\
         Examples:\n\
         \x20 zilliz external-collection refresh list\n\
         \x20 zilliz external-collection refresh list --name my_external_coll\n",
        "--name", "<string>", "--database", "<string>",
    );
}

fn is_help(args: &[String]) -> bool {
    args.iter().any(|a| a == "-h" || a == "--help")
}

fn next_val<'a>(args: &'a [String], i: &mut usize) -> Option<&'a str> {
    let next_i = *i + 1;
    let val = args
        .get(next_i)
        .map(|s| s.as_str())
        .filter(|s| !s.starts_with("--"));
    if val.is_some() {
        *i = next_i;
    }
    val
}

fn make_data_plane_client(
    config_mgr: &ConfigManager,
    api_key_override: Option<&str>,
) -> Result<ApiClient> {
    let api_key =
        resolve_api_key(api_key_override, config_mgr).ok_or_else(|| ApiError::NoApiKey)?;
    let ctx = config_mgr.get_context();
    let base_url = ctx
        .endpoint
        .context("No cluster context set. Run: zilliz context set --cluster-id <id>")?;
    let mut client = ApiClient::new(api_key, base_url);
    if let Some(cid) = ctx.cluster_id {
        client = client.with_extra_query([("cluster_id", cid)]);
    }
    Ok(client)
}

async fn trigger(
    config_mgr: &ConfigManager,
    raw_args: &[String],
    output_opts: &OutputOpts<'_>,
) -> Result<()> {
    if is_help(raw_args) {
        print_trigger_help();
        return Ok(());
    }

    let mut name: Option<String> = None;
    let mut db_name: Option<String> = None;
    let mut external_source: Option<String> = None;
    let mut external_spec: Option<String> = None;
    let mut i = 0;
    while i < raw_args.len() {
        match raw_args[i].as_str() {
            "--name" => name = next_val(raw_args, &mut i).map(|s| s.to_string()),
            "--database" => db_name = next_val(raw_args, &mut i).map(|s| s.to_string()),
            "--external-source" => {
                external_source = next_val(raw_args, &mut i).map(|s| s.to_string())
            }
            "--external-spec" => external_spec = next_val(raw_args, &mut i).map(|s| s.to_string()),
            other => bail!(
                "Unknown flag '{}'. Available: --name, --database, --external-source, --external-spec",
                other
            ),
        }
        i += 1;
    }

    let name = name.context("Missing required option: --name")?;
    let mut body = json!({ "collectionName": name });
    if let Some(db) = db_name {
        body["dbName"] = json!(db);
    }
    if let Some(src) = external_source {
        body["externalSource"] = json!(src);
    }
    if let Some(spec) = external_spec {
        body["externalSpec"] = json!(spec);
    }

    let client = make_data_plane_client(config_mgr, output_opts.api_key)?;
    let result: Value = client
        .call(
            "POST",
            "/v2/vectordb/jobs/external_collection/refresh",
            None,
            Some(&body),
        )
        .await?;
    print_output_with_opts(&result, output_opts, None);
    Ok(())
}

async fn describe(
    config_mgr: &ConfigManager,
    raw_args: &[String],
    output_opts: &OutputOpts<'_>,
) -> Result<()> {
    if is_help(raw_args) {
        print_describe_help();
        return Ok(());
    }

    let mut job_id: Option<i64> = None;
    let mut i = 0;
    while i < raw_args.len() {
        match raw_args[i].as_str() {
            "--job-id" => {
                job_id = next_val(raw_args, &mut i)
                    .map(|s| s.parse::<i64>().context("--job-id must be an integer"))
                    .transpose()?;
            }
            other => bail!("Unknown flag '{}'. Available: --job-id", other),
        }
        i += 1;
    }

    let job_id = job_id.context("Missing required option: --job-id")?;
    let body = json!({ "jobId": job_id });

    let client = make_data_plane_client(config_mgr, output_opts.api_key)?;
    let result: Value = client
        .call(
            "POST",
            "/v2/vectordb/jobs/external_collection/describe",
            None,
            Some(&body),
        )
        .await?;
    print_output_with_opts(&result, output_opts, None);
    Ok(())
}

async fn list(
    config_mgr: &ConfigManager,
    raw_args: &[String],
    output_opts: &OutputOpts<'_>,
) -> Result<()> {
    if is_help(raw_args) {
        print_list_help();
        return Ok(());
    }

    let mut name: Option<String> = None;
    let mut db_name: Option<String> = None;
    let mut i = 0;
    while i < raw_args.len() {
        match raw_args[i].as_str() {
            "--name" => name = next_val(raw_args, &mut i).map(|s| s.to_string()),
            "--database" => db_name = next_val(raw_args, &mut i).map(|s| s.to_string()),
            other => bail!("Unknown flag '{}'. Available: --name, --database", other),
        }
        i += 1;
    }

    let mut body = json!({});
    let ctx_db = config_mgr.get_context().database;
    let effective_db = db_name.or(if ctx_db.is_empty() {
        None
    } else {
        Some(ctx_db)
    });
    if let Some(db) = effective_db {
        body["dbName"] = json!(db);
    }
    if let Some(n) = name {
        body["collectionName"] = json!(n);
    }

    let client = make_data_plane_client(config_mgr, output_opts.api_key)?;
    let result: Value = client
        .call(
            "POST",
            "/v2/vectordb/jobs/external_collection/list",
            None,
            Some(&body),
        )
        .await?;
    print_output_with_opts(&result, output_opts, None);
    Ok(())
}