harn-cli 0.7.43

CLI for the Harn programming language — run, test, REPL, format, and lint
use serde::Serialize;

use crate::cli::{
    OrchestratorTenantArgs, OrchestratorTenantCommand, OrchestratorTenantCreateArgs,
    OrchestratorTenantDeleteArgs, OrchestratorTenantLsArgs, OrchestratorTenantSuspendArgs,
};

use super::common::{absolutize_from_cwd, print_json};

#[derive(Serialize)]
struct TenantCreateOutput {
    tenant: harn_vm::TenantRecord,
    api_key: String,
}

pub(crate) async fn run(args: OrchestratorTenantArgs) -> Result<(), String> {
    let state_dir = absolutize_from_cwd(&args.local.state_dir)?;
    std::fs::create_dir_all(&state_dir).map_err(|error| {
        format!(
            "failed to create state dir {}: {error}",
            state_dir.display()
        )
    })?;
    let mut store = harn_vm::TenantStore::load(&state_dir)?;
    match args.command {
        OrchestratorTenantCommand::Create(create) => create_tenant(&mut store, create),
        OrchestratorTenantCommand::Ls(ls) => list_tenants(&store, ls),
        OrchestratorTenantCommand::Suspend(suspend) => suspend_tenant(&mut store, suspend),
        OrchestratorTenantCommand::Delete(delete) => delete_tenant(&mut store, delete),
    }
}

fn create_tenant(
    store: &mut harn_vm::TenantStore,
    args: OrchestratorTenantCreateArgs,
) -> Result<(), String> {
    if let Some(value) = args.daily_cost_usd {
        if value < 0.0 {
            return Err("--daily-cost-usd must be greater than or equal to 0".to_string());
        }
    }
    if let Some(value) = args.hourly_cost_usd {
        if value < 0.0 {
            return Err("--hourly-cost-usd must be greater than or equal to 0".to_string());
        }
    }
    let budget = harn_vm::TenantBudget {
        daily_cost_usd: args.daily_cost_usd,
        hourly_cost_usd: args.hourly_cost_usd,
        ingest_per_minute: args.ingest_per_minute,
        ..harn_vm::TenantBudget::default()
    };
    let (tenant, api_key) = store.create_tenant(args.id, budget)?;
    if args.json {
        return print_json(&TenantCreateOutput { tenant, api_key });
    }
    println!("created tenant {}", tenant.scope.id.0);
    println!("state: {}", tenant.scope.state_root.display());
    println!("secret namespace: {}", tenant.scope.secret_namespace);
    println!(
        "event topic prefix: {}",
        tenant.scope.event_log_topic_prefix
    );
    println!("api key: {api_key}");
    Ok(())
}

fn list_tenants(
    store: &harn_vm::TenantStore,
    args: OrchestratorTenantLsArgs,
) -> Result<(), String> {
    let tenants = store.list();
    if args.json {
        return print_json(&tenants);
    }
    if tenants.is_empty() {
        println!("no tenants");
        return Ok(());
    }
    for tenant in tenants {
        println!(
            "{}\t{:?}\t{}\tkeys={}",
            tenant.scope.id.0,
            tenant.status,
            tenant.scope.state_root.display(),
            tenant.api_keys.len()
        );
    }
    Ok(())
}

fn suspend_tenant(
    store: &mut harn_vm::TenantStore,
    args: OrchestratorTenantSuspendArgs,
) -> Result<(), String> {
    let tenant = store.suspend(&args.id)?;
    if args.json {
        return print_json(&tenant);
    }
    println!("suspended tenant {}", tenant.scope.id.0);
    Ok(())
}

fn delete_tenant(
    store: &mut harn_vm::TenantStore,
    args: OrchestratorTenantDeleteArgs,
) -> Result<(), String> {
    if !args.confirm {
        return Err("tenant delete is destructive; pass --confirm".to_string());
    }
    let tenant = store.delete(&args.id)?;
    if args.json {
        return print_json(&tenant);
    }
    println!("deleted tenant {}", tenant.scope.id.0);
    Ok(())
}