crates/knut-pipelines/src/bin/knut-spark-server.rs
//! `knut-spark-server` — bring a local Spark 4.x **Connect + SDP pipelines**
//! server up/down **entirely from Rust**, over podman's REST API.
//!
//! This binary is a thin CLI wrapper over the reusable [`knut_pipelines::server`]
//! library module ([`SparkServer`]): all the container lifecycle logic lives in
//! the lib so downstream crates (e.g. korp's demo orchestrator) can bring Spark
//! up from Rust too. The zero-shell charter is the lib's: no `sh`, no
//! `podman`/`docker` CLI, no [`std::process::Command`] — the container lifecycle
//! goes through the Docker/podman HTTP API, routed THROUGH draupnir's ONE OCI
//! engine (no second bollard engine in knut).
//!
//! Gated behind the `server` feature so the default build stays lean:
//! ```bash
//! cargo run -p knut-pipelines --features server --bin knut-spark-server -- up
//! ```
//!
//! Subcommands:
//! - `up` — pull the image if absent, create+start a detached container
//! publishing 15002, wait for the connect-server ready line, print
//! `ready: sc://localhost:15002`, and RETURN (server stays running).
//! - `stop` — stop + remove the container.
//! - `logs` — stream the server logs.
//! - `status` — report running / port.
use clap::{Parser, Subcommand};
use knut_pipelines::server::{ServerStatus, SparkServer};
#[derive(Parser)]
#[command(
name = "knut-spark-server",
about = "Bring a local Spark Connect + SDP pipelines server up/down via podman's REST API (pure Rust, zero-shell)",
// `v<pkg> (<git-sha>)`, stamped pure-fs by build.rs → `knut-spark-server v<ver> (<hash>)`.
version = knut_pipelines::VERSION_LINE
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Pull (if absent), create and start the server detached; wait until ready.
Up,
/// Stop and remove the server container.
Stop,
/// Stream the server's logs (follow).
Logs,
/// Report whether the server is running, and on which port.
Status,
}
#[tokio::main]
async fn main() -> std::process::ExitCode {
let cli = Cli::parse();
// `connect` prints the podman-socket prerequisite hint on failure.
let server = match SparkServer::connect() {
Ok(s) => s,
Err(_) => return std::process::ExitCode::FAILURE,
};
let result = match cli.command {
Command::Up => match server.up().await {
Ok(url) => {
println!("ready: {url}");
println!(
"drive: knutpipe pipeline {url} --spec crates/knut-pipelines/examples/pipeline.json --storage file:///tmp/knut_pipeline"
);
Ok(())
}
Err(e) => Err(e),
},
Command::Stop => server.stop().await.map(|()| println!("stopped {}", NAME)),
Command::Logs => server.logs().await,
Command::Status => match server.status().await {
Ok(ServerStatus::Running { url }) => {
println!("running: {NAME} · {url}");
Ok(())
}
Ok(ServerStatus::Stopped) => {
println!("stopped: {NAME} exists but is not running");
Ok(())
}
Ok(ServerStatus::Absent) => {
println!("absent: no {NAME} container (run `up` to start one)");
Ok(())
}
Err(e) => Err(e),
},
};
match result {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
std::process::ExitCode::FAILURE
}
}
}
/// The managed container name, for the CLI's status/stop messages (mirrors the
/// lib default).
const NAME: &str = knut_pipelines::server::DEFAULT_NAME;