use anyhow::{Context, Result};
use clap::Args;
use kanade_shared::manifest::{FanoutPlan, Target};
use serde::Deserialize;
use tracing::info;
#[derive(Args, Debug)]
pub struct ExecArgs {
pub job_id: String,
#[arg(long, conflicts_with_all = ["groups", "pcs"])]
pub all: bool,
#[arg(long, value_delimiter = ',')]
pub groups: Vec<String>,
#[arg(long, value_delimiter = ',')]
pub pcs: Vec<String>,
#[arg(long)]
pub jitter: Option<String>,
}
#[derive(Deserialize, Debug)]
struct ExecResponse {
exec_id: String,
job_id: String,
version: String,
target_count: u32,
subjects: Vec<String>,
}
pub async fn execute(backend_url: &str, args: ExecArgs) -> Result<()> {
let target = Target {
all: args.all,
groups: args.groups,
pcs: args.pcs,
};
if !target.is_specified() {
anyhow::bail!(
"no target — pass --all, --groups <a,b>, or --pcs <pc1,pc2> (or use a Schedule for waves)"
);
}
let plan = FanoutPlan {
target,
rollout: None,
jitter: args.jitter,
};
info!(job_id = %args.job_id, "executing");
let url = format!(
"{}/api/exec/{}",
backend_url.trim_end_matches('/'),
args.job_id,
);
let resp = crate::http_client::authed_client()?
.post(&url)
.json(&plan)
.send()
.await
.with_context(|| format!("POST {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("exec rejected: {status} — {body}");
}
let payload: ExecResponse = resp.json().await.context("decode exec response")?;
println!("exec_id : {}", payload.exec_id);
println!("job_id : {}", payload.job_id);
println!("version : {}", payload.version);
println!("target_count : {}", payload.target_count);
println!("subjects :");
for s in &payload.subjects {
println!(" - {s}");
}
Ok(())
}