Adds an `operator_application()` helper that builds an
`ArgoApplication` targeting
`oci://<registry>/<project>/harmony-fleet-operator:<chart_version>`.
main.rs composes it into `ArgoHelmScore { argo_apps: vec![...] }` —
no new score types, no wrapper module.
CLI gains `--use-argo` to swap the operator path from direct-helm
to Argo, plus `--operator-chart-version` as the CD knob CI re-runs
to deploy / upgrade / roll back.
Splits `ArgoApplication.destination_namespace` from
`metadata.namespace` so the Application CR can live in `argocd`
while syncing chart resources into `fleet-system` — `to_yaml`
previously collapsed them, breaking any cross-namespace deploy.
NATS + agent stay on the direct path in v1; flattens
`harmony_cli::Args` into the binary's CliConfig so `--yes` makes it
through one argv parse.
Tested on a fresh k3d cluster against the published
`hub.nationtech.io/harmony/harmony-fleet-operator:0.0.1`.
192 lines
6.1 KiB
Rust
192 lines
6.1 KiB
Rust
//! `harmony-fleet-deploy` — deploy the fleet stack to a cluster.
|
|
|
|
use anyhow::{Context, Result};
|
|
use clap::Parser;
|
|
use harmony::inventory::Inventory;
|
|
use harmony::modules::application::features::ArgoHelmScore;
|
|
use harmony::score::Score;
|
|
use harmony::topology::K8sAnywhereTopology;
|
|
use harmony_cli::Args as HarmonyCliArgs;
|
|
use harmony_fleet_deploy::nats::UserPassCredentials;
|
|
use harmony_fleet_deploy::{
|
|
FleetAgentScore, FleetNatsScore, FleetOperatorScore, agent::PodTarget, argo,
|
|
};
|
|
|
|
#[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>,
|
|
|
|
/// Deploy the operator via Argo CD instead of harmony-direct
|
|
/// helm. Re-run with a different `--operator-chart-version` to
|
|
/// upgrade or roll back.
|
|
#[arg(long, env = "HARMONY_FLEET_USE_ARGO")]
|
|
use_argo: bool,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "HARMONY_FLEET_OPERATOR_CHART_REGISTRY",
|
|
default_value = "hub.nationtech.io"
|
|
)]
|
|
operator_chart_registry: String,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "HARMONY_FLEET_OPERATOR_CHART_PROJECT",
|
|
default_value = "harmony"
|
|
)]
|
|
operator_chart_project: String,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "HARMONY_FLEET_OPERATOR_CHART_VERSION",
|
|
default_value = "0.0.1"
|
|
)]
|
|
operator_chart_version: String,
|
|
|
|
#[arg(long, env = "HARMONY_FLEET_ARGO_NAMESPACE", default_value = "argocd")]
|
|
argo_namespace: String,
|
|
|
|
// Flattened so a single argv parse covers both this CLI and
|
|
// harmony_cli's `--yes` / `--filter` / `--all` / etc.
|
|
#[command(flatten)]
|
|
harmony_cli: HarmonyCliArgs,
|
|
}
|
|
|
|
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 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,
|
|
),
|
|
);
|
|
|
|
// `--use-argo` swaps the operator path only. NATS + agent stay
|
|
// direct in v1.
|
|
let scores: Vec<Box<dyn Score<K8sAnywhereTopology>>> = if cli.use_argo {
|
|
let argo = ArgoHelmScore {
|
|
namespace: cli.argo_namespace.clone(),
|
|
openshift: false,
|
|
ingress_class_name: None,
|
|
argo_apps: vec![argo::operator_application(
|
|
&cli.namespace,
|
|
&cli.operator_chart_registry,
|
|
&cli.operator_chart_project,
|
|
&cli.operator_chart_version,
|
|
)],
|
|
};
|
|
vec![Box::new(nats), Box::new(argo), Box::new(agent)]
|
|
} else {
|
|
let operator = FleetOperatorScore::new()
|
|
.namespace(cli.namespace.clone())
|
|
.image(cli.operator_image.clone())
|
|
.nats_url(nats.in_cluster_url());
|
|
vec![Box::new(nats), Box::new(operator), Box::new(agent)]
|
|
};
|
|
|
|
harmony_cli::run(
|
|
Inventory::autoload(),
|
|
K8sAnywhereTopology::from_env(),
|
|
scores,
|
|
Some(cli.harmony_cli),
|
|
)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("{e}"))
|
|
}
|