use std::io::{self, BufRead, Write};
use anyhow::{bail, Context, Result};
use serde_json::json;
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};
struct CreateOpts {
name: Option<String>,
cluster_type: Option<String>,
project_id: Option<String>,
region: Option<String>,
cu_type: Option<String>,
cu_size: Option<i64>,
plan: Option<String>,
}
fn parse_create_args(raw_args: &[String]) -> Result<CreateOpts> {
let mut opts = CreateOpts {
name: None,
cluster_type: None,
project_id: None,
region: None,
cu_type: None,
cu_size: None,
plan: None,
};
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--name" => {
i += 1;
opts.name = raw_args.get(i).cloned();
}
"--type" => {
i += 1;
opts.cluster_type = raw_args.get(i).cloned();
}
"--project-id" => {
i += 1;
opts.project_id = raw_args.get(i).cloned();
}
"--region" => {
i += 1;
opts.region = raw_args.get(i).cloned();
}
"--cu-type" => {
i += 1;
opts.cu_type = raw_args.get(i).cloned();
}
"--cu-size" => {
i += 1;
opts.cu_size = raw_args.get(i).and_then(|s| s.parse().ok());
}
"--plan" => {
i += 1;
opts.plan = raw_args.get(i).cloned();
}
_ => {}
}
i += 1;
}
Ok(opts)
}
pub async fn create(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if raw_args.iter().any(|a| a == "-h" || a == "--help") {
print!(
"Create a new cluster.\n\n\
Usage: zilliz cluster create [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:30}Cluster name\n\
\x20 {:24}{:30}Cluster type: free, serverless, dedicated\n\
\x20 {:24}{:30}Project ID (prompted if omitted)\n\
\x20 {:24}{:30}Region ID (prompted if omitted)\n\
\x20 {:24}{:30}CU type (dedicated only): Performance-optimized, Capacity-optimized\n\
\x20 {:24}{:30}Number of CUs (dedicated only)\n\
\x20 {:24}{:30}Billing plan: Standard, Enterprise, BusinessCritical\n",
"--name",
"<string> (required)",
"--type",
"<string> (required)",
"--project-id",
"<string>",
"--region",
"<string>",
"--cu-type",
"<string>",
"--cu-size",
"<integer>",
"--plan",
"<string>",
);
return Ok(());
}
let mut opts = parse_create_args(raw_args)?;
let api_key = resolve_api_key(output_opts.api_key, config_mgr).ok_or(ApiError::NoApiKey)?;
let base_url =
super::endpoint::resolve_control_plane_url(config_mgr, &models.control_plane, None);
let client = ApiClient::new(api_key, base_url);
let name = opts.name.take().context("--name is required")?;
let cluster_type = opts
.cluster_type
.take()
.context("--type is required (serverless, free, or dedicated)")?;
let project_id = match opts.project_id.take() {
Some(id) => id,
None => prompt_project(&client).await?,
};
let region = match opts.region.take() {
Some(r) => r,
None => prompt_region(&client, &cluster_type.to_lowercase()).await?,
};
let (path, body) = match cluster_type.to_lowercase().as_str() {
"serverless" => (
"/v2/clusters/createServerless".to_string(),
json!({
"clusterName": name,
"projectId": project_id,
"regionId": region,
}),
),
"free" => (
"/v2/clusters/createFree".to_string(),
json!({
"clusterName": name,
"projectId": project_id,
"regionId": region,
}),
),
"dedicated" => {
let cu_type = opts
.cu_type
.take()
.context("--cu-type is required for dedicated clusters (Performance-optimized or Capacity-optimized)")?;
let cu_size = opts
.cu_size
.context("--cu-size is required for dedicated clusters")?;
let mut body = json!({
"clusterName": name,
"projectId": project_id,
"regionId": region,
"cuType": cu_type,
"cuSize": cu_size,
});
if let Some(plan) = opts.plan.take() {
body["plan"] = json!(plan);
}
("/v2/clusters/createDedicated".to_string(), body)
}
_ => bail!(
"Invalid cluster type '{}'. Use: serverless, free, or dedicated",
cluster_type
),
};
let result = client.call("POST", &path, None, Some(&body)).await?;
print_output_with_opts(&result, output_opts, None);
Ok(())
}
async fn prompt_project(client: &ApiClient) -> Result<String> {
let result = client.call("GET", "/v2/projects", None, None).await?;
let projects = result
.as_array()
.or_else(|| result.get("data").and_then(|v| v.as_array()))
.context("Failed to fetch projects")?;
if projects.is_empty() {
bail!("No projects found. Create a project first.");
}
println!("Available projects:");
for (i, p) in projects.iter().enumerate() {
let name = p.get("projectName").and_then(|v| v.as_str()).unwrap_or("?");
let id = p.get("projectId").and_then(|v| v.as_str()).unwrap_or("?");
println!(" [{}] {} ({})", i + 1, name, id);
}
print!("Select project [1-{}]: ", projects.len());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().lock().read_line(&mut input)?;
let idx: usize = input.trim().parse().context("Invalid selection")?;
if idx < 1 || idx > projects.len() {
bail!("Selection out of range");
}
projects[idx - 1]
.get("projectId")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.context("Project has no ID")
}
async fn prompt_region(client: &ApiClient, cluster_type: &str) -> Result<String> {
let result = client.call("GET", "/v2/regions", None, None).await?;
let all_regions = result
.as_array()
.or_else(|| result.get("data").and_then(|v| v.as_array()))
.context("Failed to fetch regions")?;
if all_regions.is_empty() {
bail!("No regions available.");
}
let regions: Vec<&serde_json::Value> = all_regions
.iter()
.filter(|r| region_supports_cluster_type(r, cluster_type))
.collect();
if regions.is_empty() {
bail!("No regions available for cluster type '{}'.", cluster_type);
}
println!("Available regions for cluster type '{}':", cluster_type);
for (i, r) in regions.iter().enumerate() {
let id = r.get("regionId").and_then(|v| v.as_str()).unwrap_or("?");
println!(" [{}] {}", i + 1, id);
}
print!("Select region [1-{}]: ", regions.len());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().lock().read_line(&mut input)?;
let idx: usize = input.trim().parse().context("Invalid selection")?;
if idx < 1 || idx > regions.len() {
bail!("Selection out of range");
}
regions[idx - 1]
.get("regionId")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.context("Region has no ID")
}
pub fn region_supports_cluster_type(region: &serde_json::Value, cluster_type: &str) -> bool {
match region.get("supportedClusterTypes") {
Some(serde_json::Value::Array(types)) => types
.iter()
.filter_map(|v| v.as_str())
.any(|t| t.eq_ignore_ascii_case(cluster_type)),
_ => true,
}
}