Files
harmony/examples/fleet_server_install/src/main.rs
2026-05-20 12:03:19 -04:00

193 lines
7.3 KiB
Rust

//! Install the harmony fleet server-side stack into the cluster
//! `KUBECONFIG` points at: NATS + the harmony fleet operator (CRDs +
//! RBAC + Deployment), and optionally a central Zitadel OIDC
//! identity provider, via [`FleetServerScore`].
//!
//! This is the framework-side replacement for the
//! `example_fleet_nats_install`, `harmony-fleet-operator chart`,
//! and `helm install` chain that the load-test harness used to
//! drive by hand.
//!
//! Typical usage (operator + NATS only):
//!
//! KUBECONFIG=$KUBECFG cargo run -q -p example_fleet_server_install -- \
//! --operator-image hub.nationtech.io/harmony/harmony-fleet-operator:dev
//!
//! Including Zitadel:
//!
//! KUBECONFIG=$KUBECFG cargo run -q -p example_fleet_server_install -- \
//! --operator-image … \
//! --zitadel-host zitadel.localhost
//!
//! Behaviour:
//! - Installs single-node NATS (JetStream) into `--nats-namespace`
//! using `NatsBasicScore`, exposed per `--nats-expose`.
//! - Installs the operator chart into `--operator-namespace` via
//! `FleetOperatorScore` (which renders the chart in a tempdir
//! and helm-installs it).
//! - When `--zitadel-host` is set, also runs `ZitadelScore`:
//! provisions a CNPG PostgreSQL cluster + the upstream
//! `zitadel/zitadel` helm chart with distribution-aware ingress.
//! Defaults to HTTPS unless host endswith `.localhost` or
//! `--zitadel-insecure` is passed.
//! - Idempotent: re-running on an existing install short-circuits
//! at `HelmChartScore::find_installed_release`.
//!
//! Topology: `K8sAnywhereTopology::from_env()`. This requires `KUBECONFIG`
//! to be set and runs `CertificateManagementScore` as part of
//! `ensure_ready` — i.e. it installs cert-manager into the cluster on
//! first run. Cert-manager is needed for Zitadel's ingress TLS in
//! production; for k3d dev it's still installed but unused.
//!
//! Output is driven by `harmony_cli::run`, which wires up the
//! framework's standard logger + reporter — emoji-tagged progress
//! lines per Score, plus an end-of-run summary listing the
//! `Outcome.details` from each Score.
use anyhow::Result;
use clap::Parser;
use harmony::inventory::Inventory;
use harmony::modules::nats::NatsBasicScore;
use harmony::modules::zitadel::ZitadelScore;
use harmony::score::Score;
use harmony::topology::K8sAnywhereTopology;
use harmony_fleet_deploy::FleetOperatorScore;
#[derive(Parser, Debug)]
#[command(
name = "fleet_server_install",
about = "Install the harmony fleet server-side stack (NATS + operator [+ Zitadel])"
)]
struct Cli {
/// Namespace for the NATS Deployment + Service.
#[arg(long, default_value = "fleet-system")]
nats_namespace: String,
/// Resource name for the NATS release.
#[arg(long, default_value = "fleet-nats")]
nats_name: String,
/// NATS service exposure mode. `load-balancer` pairs with k3d's
/// `-p PORT:PORT@loadbalancer`. `node-port` requires the port be
/// in the apiserver's nodeport range (default 30000-32767).
#[arg(long, value_enum, default_value_t = NatsExpose::LoadBalancer)]
nats_expose: NatsExpose,
/// NodePort when `--nats-expose=node-port`. Ignored otherwise.
#[arg(long, default_value_t = 30422)]
nats_node_port: i32,
/// Optional NATS image override (`repository:tag`).
#[arg(long)]
nats_image: Option<String>,
/// Namespace the operator runs in.
#[arg(long, default_value = "fleet-system")]
operator_namespace: String,
/// Helm release name for the operator chart.
#[arg(long, default_value = "harmony-fleet-operator")]
operator_release: String,
/// Operator container image (`repository:tag`).
#[arg(
long,
default_value = "hub.nationtech.io/harmony/harmony-fleet-operator:dev"
)]
operator_image: String,
/// Image pull policy for the operator Deployment.
#[arg(long, default_value = "IfNotPresent")]
operator_image_pull_policy: String,
/// `RUST_LOG` value injected into the operator pod's env.
#[arg(long, default_value = "info,kube_runtime=warn")]
log_level: String,
/// Hostname Zitadel should answer on. When set, Zitadel + its
/// PostgreSQL cluster are installed alongside the operator.
/// When unset, the Zitadel install is skipped entirely.
#[arg(long)]
zitadel_host: Option<String>,
/// Zitadel chart version (matches `zitadel/zitadel` upstream tags).
#[arg(long, default_value = "v4.12.1")]
zitadel_version: String,
/// Force HTTP instead of HTTPS for the Zitadel ingress. Defaults
/// to true (HTTP) when `--zitadel-host` endswith `.localhost`,
/// false otherwise.
#[arg(long)]
zitadel_insecure: bool,
}
#[derive(Clone, Debug, clap::ValueEnum)]
enum NatsExpose {
ClusterIp,
NodePort,
LoadBalancer,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let topology = K8sAnywhereTopology::from_env();
let mut nats = NatsBasicScore::new(&cli.nats_name, &cli.nats_namespace);
match cli.nats_expose {
NatsExpose::ClusterIp => {}
NatsExpose::NodePort => nats = nats.node_port(cli.nats_node_port),
NatsExpose::LoadBalancer => nats = nats.load_balancer(),
}
if let Some(image) = cli.nats_image {
nats = nats.image(image);
}
// Point the operator at NATS via the in-cluster service DNS the
// NatsBasicScore install creates. ClusterIP and LoadBalancer both
// expose the same `<release>.<namespace>:4222` for in-cluster
// callers.
let nats_url = format!("nats://{}.{}:4222", cli.nats_name, cli.nats_namespace);
let operator = FleetOperatorScore::new()
.namespace(&cli.operator_namespace)
.release_name(&cli.operator_release)
.image(&cli.operator_image)
.image_pull_policy(&cli.operator_image_pull_policy)
.nats_url(&nats_url)
.log_level(&cli.log_level);
// FleetServerScore now takes NatsK8sScore (auth-callout-aware,
// OKD-Route-aware) — see `fleet_staging_install` for the
// production composition. This simpler example registers the
// inner Scores directly so it can keep using the basic NATS
// helm chart for k3d-style local installs.
let mut scores: Vec<Box<dyn Score<K8sAnywhereTopology>>> =
vec![Box::new(nats), Box::new(operator)];
if let Some(host) = cli.zitadel_host {
// Default external_secure logic: HTTPS unless the host is a
// .localhost / .test development hostname or --zitadel-insecure
// was explicitly set.
let external_secure =
!cli.zitadel_insecure && !host.ends_with(".localhost") && !host.ends_with(".test");
scores.push(Box::new(ZitadelScore {
host,
zitadel_version: cli.zitadel_version,
external_secure,
external_port: None,
..Default::default()
}));
}
// We've already parsed our own Cli; pass `Some(harmony_cli::Args)`
// with dev-friendly defaults (no confirmation prompt, run every
// registered score) so harmony_cli doesn't try to re-parse argv.
harmony_cli::run(
Inventory::empty(),
topology,
scores,
Some(harmony_cli::Args {
yes: true,
filter: None,
interactive: false,
all: true,
number: 0,
list: false,
}),
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
}