Caller must pass `UserPassCredentials` to `FleetNatsScore::user_pass` — no more `e2e-admin`/`e2e-device` defaults shipped in the library. The deploy binary reads `HARMONY_FLEET_*` env vars (default namespace `harmony-fleet-system`) and fails fast when NATS creds aren't set. Also: `style/dist/` gitignored, `manual_mint/mint.py` moved next to `nats/callout/` with README + secrets gitignore (the real RSA key that was sitting untracked has been removed), `architecture_review.md` moved to `docs/adr/drafts/024-`, three low-value ROADMAP docs deleted. Updates pre-merge checklist (§1.6, §1.8, §3.1, §5).
154 lines
5.3 KiB
Rust
154 lines
5.3 KiB
Rust
//! `harmony-fleet-deploy` — deploy the fleet stack to a cluster.
|
|
//!
|
|
//! Built on `harmony_cli::run` like the rest of the workspace's
|
|
//! deploy binaries (`harmony_agent_deploy`, …). The CLI offers a
|
|
//! minimal env-driven config and hands off to `harmony_cli`, which
|
|
//! provides the standard `--filter` / `--all` / `--list` selection
|
|
//! surface; pick a single component by filter or run them all.
|
|
//!
|
|
//! Topology default is [`K8sAnywhereTopology::from_env`] — local k3d
|
|
//! when `HARMONY_USE_LOCAL_K3D` flips, otherwise whatever cluster
|
|
//! `KUBECONFIG` points at. Per ADR-023 the binary's full set of
|
|
//! topologies is compile-time; for the moment only `K8sAnywhere` is
|
|
//! wired in, with `K8sBareTopology` planned for the next iteration.
|
|
//!
|
|
//! What the binary owns: assembling the Scores from environment
|
|
//! input. What it does **not** own: any handrolled k8s manifests,
|
|
//! any imperative bring-up loops, any auth secret rendering — that
|
|
//! all sits inside the `*Score` impls in [`harmony_fleet_deploy`].
|
|
|
|
use anyhow::{Context, Result};
|
|
use clap::Parser;
|
|
use harmony::inventory::Inventory;
|
|
use harmony::score::Score;
|
|
use harmony::topology::K8sAnywhereTopology;
|
|
use harmony_fleet_deploy::nats::UserPassCredentials;
|
|
use harmony_fleet_deploy::{FleetAgentScore, FleetNatsScore, FleetOperatorScore, agent::PodTarget};
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(
|
|
name = "harmony-fleet-deploy",
|
|
about = "Deploy the harmony fleet stack to a Kubernetes cluster"
|
|
)]
|
|
struct CliConfig {
|
|
/// Namespace every component lands in. Override with
|
|
/// `HARMONY_FLEET_NAMESPACE`.
|
|
#[arg(
|
|
long,
|
|
env = "HARMONY_FLEET_NAMESPACE",
|
|
default_value = "harmony-fleet-system"
|
|
)]
|
|
namespace: String,
|
|
|
|
/// Operator image to pull. Defaults to the dev tag the
|
|
/// `harmony-fleet-operator/Dockerfile` produces.
|
|
#[arg(
|
|
long,
|
|
env = "HARMONY_FLEET_OPERATOR_IMAGE",
|
|
default_value = "localhost/harmony-fleet-operator:dev"
|
|
)]
|
|
operator_image: String,
|
|
|
|
/// Agent image to pull. The e2e harness sideloads
|
|
/// `localhost/harmony-fleet-agent:e2e`; production override via
|
|
/// the env var.
|
|
#[arg(
|
|
long,
|
|
env = "HARMONY_FLEET_AGENT_IMAGE",
|
|
default_value = "localhost/harmony-fleet-agent:e2e"
|
|
)]
|
|
agent_image: String,
|
|
|
|
/// Device id for the agent Pod this deploy provisions. Production
|
|
/// will likely deploy multiple agents; for the minimal CLI v1 the
|
|
/// caller runs the binary once per device.
|
|
#[arg(
|
|
long,
|
|
env = "HARMONY_FLEET_AGENT_DEVICE_ID",
|
|
default_value = "fleet-agent-00"
|
|
)]
|
|
agent_device_id: String,
|
|
|
|
/// Host-side NodePort the NATS Service exposes.
|
|
#[arg(long, env = "HARMONY_FLEET_NATS_NODE_PORT", default_value_t = 30423)]
|
|
nats_node_port: u16,
|
|
|
|
/// NATS admin user (full pub/sub on the `APP` account). Required.
|
|
#[arg(long, env = "HARMONY_FLEET_NATS_ADMIN_USER")]
|
|
nats_admin_user: Option<String>,
|
|
|
|
/// NATS admin password. Required.
|
|
#[arg(long, env = "HARMONY_FLEET_NATS_ADMIN_PASS")]
|
|
nats_admin_pass: Option<String>,
|
|
|
|
/// NATS device user (limited pub/sub). Required.
|
|
#[arg(long, env = "HARMONY_FLEET_NATS_DEVICE_USER")]
|
|
nats_device_user: Option<String>,
|
|
|
|
/// NATS device password. Required.
|
|
#[arg(long, env = "HARMONY_FLEET_NATS_DEVICE_PASS")]
|
|
nats_device_pass: Option<String>,
|
|
}
|
|
|
|
impl CliConfig {
|
|
/// Build the NATS credentials struct or fail with an actionable
|
|
/// error message naming the missing env var. No dev defaults —
|
|
/// the deploy binary refuses to ship known-weak passwords.
|
|
fn nats_creds(&self) -> Result<UserPassCredentials> {
|
|
Ok(UserPassCredentials {
|
|
admin_user: self
|
|
.nats_admin_user
|
|
.clone()
|
|
.context("HARMONY_FLEET_NATS_ADMIN_USER must be set")?,
|
|
admin_pass: self
|
|
.nats_admin_pass
|
|
.clone()
|
|
.context("HARMONY_FLEET_NATS_ADMIN_PASS must be set")?,
|
|
device_user: self
|
|
.nats_device_user
|
|
.clone()
|
|
.context("HARMONY_FLEET_NATS_DEVICE_USER must be set")?,
|
|
device_pass: self
|
|
.nats_device_pass
|
|
.clone()
|
|
.context("HARMONY_FLEET_NATS_DEVICE_PASS must be set")?,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let cli = CliConfig::parse();
|
|
|
|
let creds = cli.nats_creds()?;
|
|
let device_user = creds.device_user.clone();
|
|
let device_pass = creds.device_pass.clone();
|
|
let nats = FleetNatsScore::user_pass(cli.namespace.clone(), cli.nats_node_port, creds);
|
|
let operator = FleetOperatorScore::new()
|
|
.namespace(cli.namespace.clone())
|
|
.image(cli.operator_image.clone())
|
|
.nats_url(nats.in_cluster_url());
|
|
let agent = FleetAgentScore::pod(
|
|
cli.namespace.clone(),
|
|
PodTarget::user_pass(
|
|
cli.agent_device_id.clone(),
|
|
cli.agent_image.clone(),
|
|
nats.in_cluster_url(),
|
|
device_user,
|
|
device_pass,
|
|
),
|
|
);
|
|
|
|
let scores: Vec<Box<dyn Score<K8sAnywhereTopology>>> =
|
|
vec![Box::new(nats), Box::new(operator), Box::new(agent)];
|
|
|
|
harmony_cli::run(
|
|
Inventory::autoload(),
|
|
K8sAnywhereTopology::from_env(),
|
|
scores,
|
|
None,
|
|
)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("{e}"))
|
|
}
|