All checks were successful
Run Check Script / check (pull_request) Successful in 2m25s
The IoT vocabulary was anchoring the codebase to one customer's
domain. The reconciler pattern is generic — operator in k8s, NATS
KV as desired-state bus, agents reconciling podman / OKD / KVM /
anything that can register. "Fleet" captures that neutrally; IoT
stays acknowledged in docs as the first customer use case.
Done now, while nothing is deployed. After a partner fleet lands,
changing the CRD group alone is a multi-quarter migration.
Scope (nothing left over):
Paths + crates
- iot/ → fleet/
- iot/iot-operator-v0 → fleet/harmony-fleet-operator
- iot/iot-agent-v0 → fleet/harmony-fleet-agent
- harmony/src/modules/iot → harmony/src/modules/fleet
- ROADMAP/iot_platform → ROADMAP/fleet_platform
- examples/iot_{vm_setup, load_test, nats_install} → examples/fleet_*
- -v0 suffix dropped on the operator + agent crates (semver in
Cargo.toml already tracks version)
Rust identifiers
- enum IotScore (podman score payload) → ReconcileScore
- struct IotDeviceSetupScore/Config → FleetDeviceSetupScore/Config
- InterpretName::IotDeviceSetup → InterpretName::FleetDeviceSetup
- HarmonyIotPool → HarmonyFleetPool (libvirt pool)
- HARMONY_IOT_POOL_NAME (default "harmony-iot") → HARMONY_FLEET_POOL_NAME ("harmony-fleet")
- IotSshKeypair → FleetSshKeypair
- ensure_iot_ssh_keypair / ensure_harmony_iot_pool /
check_iot_smoke_preflight_for_arch → fleet-prefixed variants
Wire / config surfaces
- CRD group `iot.nationtech.io` → `fleet.nationtech.io`
- Finalizer `iot.nationtech.io/finalizer` → `fleet.nationtech.io/finalizer`
- Shortnames iotdep/iotdevice → fleetdep/fleetdev
- Env var IOT_AGENT_CONFIG → FLEET_AGENT_CONFIG
- Env var IOT_VM_ADMIN_PASSWORD → FLEET_VM_ADMIN_PASSWORD
- Binary /usr/local/bin/iot-agent → /usr/local/bin/fleet-agent
- Systemd user `iot-agent` → `fleet-agent`
- VM admin user `iot-admin` → `fleet-admin`
Defaults
- Namespaces iot-system/iot-demo/iot-load → fleet-system/fleet-demo/fleet-load
- Helm release iot-nats → fleet-nats
- Helm release iot-operator-v0 → harmony-fleet-operator
- Container image localhost/iot-operator-v0:latest →
localhost/harmony-fleet-operator:latest
- On-disk cache $HARMONY_DATA_DIR/iot/ → $HARMONY_DATA_DIR/fleet/
(cloud-images, ssh keypairs, libvirt pool)
What stayed
- harmony-reconciler-contracts — already neutrally named
- Wire types (DeviceInfo, DeploymentState, HeartbeatPayload,
DeploymentName) — already neutral
- KV buckets (device-info, device-state, device-heartbeat,
desired-state) — already neutral
- CRD kind names (Deployment, Device) — already neutral
- NatsBasicScore / NatsHelmChartScore / HelmChart / etc. —
framework-scope, unchanged
Verification
- cargo check --workspace --all-targets: clean
- All harmony lib tests (114), fleet-operator (6), fleet-agent
(7), harmony-reconciler-contracts (13): green
- End-to-end load-test (20 devices / 3 CRs / 20s under
fleet/scripts/load-test.sh): PASS. Image built as
localhost/harmony-fleet-operator:latest, chart installed as
release harmony-fleet-operator in namespace fleet-system,
all CR aggregates correct.
Zero stragglers: grep across the tree for \biot\b / IOT_ /
\bIot[A-Z] returns empty (excluding docs explicitly talking about
IoT as the first customer's domain).
95 lines
3.5 KiB
Rust
95 lines
3.5 KiB
Rust
//! NATS JetStream KV bucket names and key formats used by the
|
|
//! harmony reconciler pattern.
|
|
//!
|
|
//! Hard-coded literals previously lived in five places (agent and
|
|
//! operator main.rs, operator deploy YAML, two smoke scripts).
|
|
//! Changing any of them meant hunting for the others. They now live
|
|
//! here; agent + operator consume the constants directly, and smoke
|
|
//! scripts grep for the literal values locked in the tests below.
|
|
|
|
use crate::fleet::DeploymentName;
|
|
|
|
/// Operator-written bucket. One entry per `(device, deployment)` pair.
|
|
/// Values are the JSON-serialized Score envelope — today
|
|
/// `harmony::modules::podman::ReconcileScore`, tomorrow any variant of
|
|
/// a polymorphic `Score` enum the framework ships.
|
|
pub const BUCKET_DESIRED_STATE: &str = "desired-state";
|
|
|
|
/// Static-ish per-device facts: routing labels, inventory, agent
|
|
/// version. Agent rewrites the entry on startup and whenever its
|
|
/// labels change. Key format: `info.<device_id>`.
|
|
pub const BUCKET_DEVICE_INFO: &str = "device-info";
|
|
|
|
/// Current reconcile phase for each `(device, deployment)` pair.
|
|
/// Agent writes on phase transition; operator watches this bucket
|
|
/// to drive CR `.status.aggregate`. Authoritative source of truth
|
|
/// for "what's running where." Key format:
|
|
/// `state.<device_id>.<deployment>`.
|
|
pub const BUCKET_DEVICE_STATE: &str = "device-state";
|
|
|
|
/// Tiny liveness ping from each device every N seconds. Separate
|
|
/// from [`BUCKET_DEVICE_STATE`] so routine heartbeats don't churn
|
|
/// the state bucket. Key format: `heartbeat.<device_id>`.
|
|
pub const BUCKET_DEVICE_HEARTBEAT: &str = "device-heartbeat";
|
|
|
|
/// KV key for a `(device, deployment)` pair in [`BUCKET_DESIRED_STATE`].
|
|
/// Format: `<device>.<deployment>`.
|
|
pub fn desired_state_key(device_id: &str, deployment_name: &DeploymentName) -> String {
|
|
format!("{device_id}.{}", deployment_name.as_str())
|
|
}
|
|
|
|
/// KV key for a device's `DeviceInfo` entry in [`BUCKET_DEVICE_INFO`].
|
|
/// Format: `info.<device_id>`.
|
|
pub fn device_info_key(device_id: &str) -> String {
|
|
format!("info.{device_id}")
|
|
}
|
|
|
|
/// KV key for a `(device, deployment)` state entry in
|
|
/// [`BUCKET_DEVICE_STATE`]. Format: `state.<device_id>.<deployment>`.
|
|
pub fn device_state_key(device_id: &str, deployment_name: &DeploymentName) -> String {
|
|
format!("state.{device_id}.{}", deployment_name.as_str())
|
|
}
|
|
|
|
/// KV key for a device's liveness entry in
|
|
/// [`BUCKET_DEVICE_HEARTBEAT`]. Format: `heartbeat.<device_id>`.
|
|
pub fn device_heartbeat_key(device_id: &str) -> String {
|
|
format!("heartbeat.{device_id}")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn dn(s: &str) -> crate::DeploymentName {
|
|
crate::DeploymentName::try_new(s).expect("valid")
|
|
}
|
|
|
|
#[test]
|
|
fn desired_state_key_format() {
|
|
assert_eq!(
|
|
desired_state_key("pi-01", &dn("hello-web")),
|
|
"pi-01.hello-web"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bucket_names_stable() {
|
|
// Flipping these is a cross-component break — operator,
|
|
// agent, and smoke scripts all grep for the literal values.
|
|
assert_eq!(BUCKET_DESIRED_STATE, "desired-state");
|
|
assert_eq!(BUCKET_DEVICE_INFO, "device-info");
|
|
assert_eq!(BUCKET_DEVICE_STATE, "device-state");
|
|
assert_eq!(BUCKET_DEVICE_HEARTBEAT, "device-heartbeat");
|
|
}
|
|
|
|
#[test]
|
|
fn key_formats() {
|
|
assert_eq!(device_info_key("pi-01"), "info.pi-01");
|
|
assert_eq!(
|
|
device_state_key("pi-01", &dn("hello-web")),
|
|
"state.pi-01.hello-web"
|
|
);
|
|
assert_eq!(device_heartbeat_key("pi-01"), "heartbeat.pi-01");
|
|
}
|
|
}
|