fix(cli): simplify running the CLI by hiding the maestro inside the implemtation (#93)
All checks were successful
Run Check Script / check (push) Successful in -46s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 4m36s

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/93
This commit is contained in:
Ian Letourneau 2025-08-04 20:59:07 +00:00
parent 06aab1f57f
commit 54990cd1a5
10 changed files with 99 additions and 95 deletions

View File

@ -1,5 +1,6 @@
# Harmony : Open-source infrastructure orchestration that treats your platform like first-class code. # Harmony : Open-source infrastructure orchestration that treats your platform like first-class code
*By [NationTech](https://nationtech.io)*
_By [NationTech](https://nationtech.io)_
[![Build](https://git.nationtech.io/NationTech/harmony/actions/workflows/check.yml/badge.svg)](https://git.nationtech.io/nationtech/harmony) [![Build](https://git.nationtech.io/NationTech/harmony/actions/workflows/check.yml/badge.svg)](https://git.nationtech.io/nationtech/harmony)
[![License](https://img.shields.io/badge/license-AGPLv3-blue?style=flat-square)](LICENSE) [![License](https://img.shields.io/badge/license-AGPLv3-blue?style=flat-square)](LICENSE)
@ -24,7 +25,7 @@ From a **developer laptop** to a **global production cluster**, a single **sourc
Infrastructure is essential, but it shouldnt be your core business. Harmony is built on three guiding principles that make modern platforms reliable, repeatable, and easy to reason about. Infrastructure is essential, but it shouldnt be your core business. Harmony is built on three guiding principles that make modern platforms reliable, repeatable, and easy to reason about.
| Principle | What it means for you | | Principle | What it means for you |
|-----------|-----------------------| | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Infrastructure as Resilient Code** | Replace sprawling YAML and bash scripts with type-safe Rust. Test, refactor, and version your platform just like application code. | | **Infrastructure as Resilient Code** | Replace sprawling YAML and bash scripts with type-safe Rust. Test, refactor, and version your platform just like application code. |
| **Prove It Works — Before You Deploy** | Harmony uses the compiler to verify that your applications needs match the target environments capabilities at **compile-time**, eliminating an entire class of runtime outages. | | **Prove It Works — Before You Deploy** | Harmony uses the compiler to verify that your applications needs match the target environments capabilities at **compile-time**, eliminating an entire class of runtime outages. |
| **One Unified Model** | Software and infrastructure are a single system. Harmony models them together, enabling deep automation—from bare-metal servers to Kubernetes workloads—with zero context switching. | | **One Unified Model** | Software and infrastructure are a single system. Harmony models them together, enabling deep automation—from bare-metal servers to Kubernetes workloads—with zero context switching. |
@ -63,22 +64,20 @@ async fn main() {
}, },
}; };
// 2. Pick where it should run // 2. Enhance with extra scores (monitoring, CI/CD, …)
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
Inventory::autoload(), // auto-detect hardware / kube-config
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod…
)
.await
.unwrap();
// 3. Enhance with extra scores (monitoring, CI/CD, …)
let mut monitoring = MonitoringAlertingStackScore::new(); let mut monitoring = MonitoringAlertingStackScore::new();
monitoring.namespace = Some(lamp_stack.config.namespace.clone()); monitoring.namespace = Some(lamp_stack.config.namespace.clone());
maestro.register_all(vec![Box::new(lamp_stack), Box::new(monitoring)]); // 3. Run your scores on the desired topology & inventory
harmony_cli::run(
// 4. Launch an interactive CLI / TUI Inventory::autoload(), // auto-detect hardware / kube-config
harmony_cli::init(maestro, None).await.unwrap(); K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod…
vec![
Box::new(lamp_stack),
Box::new(monitoring)
],
None
).await.unwrap();
} }
``` ```
@ -95,10 +94,10 @@ Harmony analyses the code, shows an execution plan in a TUI, and applies it once
## 3 · Core Concepts ## 3 · Core Concepts
| Term | One-liner | | Term | One-liner |
|------|-----------| | ---------------- | ---------------------------------------------------------------------------------------------------- |
| **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). | | **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). |
| **Interpret<T>** | Imperative logic that realises a `Score` on a specific environment. | | **Interpret<T>** | Imperative logic that realises a `Score` on a specific environment. |
| **Topology** | An environment (local k3d, AWS, bare-metal) exposing verified *Capabilities* (Kubernetes, DNS, …). | | **Topology** | An environment (local k3d, AWS, bare-metal) exposing verified _Capabilities_ (Kubernetes, DNS, …). |
| **Maestro** | Orchestrator that compiles Scores + Topology, ensuring all capabilities line up **at compile-time**. | | **Maestro** | Orchestrator that compiles Scores + Topology, ensuring all capabilities line up **at compile-time**. |
| **Inventory** | Optional catalogue of physical assets for bare-metal and edge deployments. | | **Inventory** | Optional catalogue of physical assets for bare-metal and edge deployments. |
@ -112,9 +111,9 @@ A visual overview is in the diagram below.
Prerequisites: Prerequisites:
* Rust - Rust
* Docker (if you deploy locally) - Docker (if you deploy locally)
* `kubectl` / `helm` for Kubernetes-based topologies - `kubectl` / `helm` for Kubernetes-based topologies
```bash ```bash
git clone https://git.nationtech.io/nationtech/harmony git clone https://git.nationtech.io/nationtech/harmony
@ -126,15 +125,15 @@ cargo build --release # builds the CLI, TUI and libraries
## 5 · Learning More ## 5 · Learning More
* **Architectural Decision Records** dive into the rationale - **Architectural Decision Records** dive into the rationale
- [ADR-001 · Why Rust](adr/001-rust.md) - [ADR-001 · Why Rust](adr/001-rust.md)
- [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md) - [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md)
- [ADR-006 · Secret Management](adr/006-secret-management.md) - [ADR-006 · Secret Management](adr/006-secret-management.md)
- [ADR-011 · Multi-Tenant Cluster](adr/011-multi-tenant-cluster.md) - [ADR-011 · Multi-Tenant Cluster](adr/011-multi-tenant-cluster.md)
* **Extending Harmony** write new Scores / Interprets, add hardware like OPNsense firewalls, or embed Harmony in your own tooling (`/docs`). - **Extending Harmony** write new Scores / Interprets, add hardware like OPNsense firewalls, or embed Harmony in your own tooling (`/docs`).
* **Community** discussions and roadmap live in [GitLab issues](https://git.nationtech.io/nationtech/harmony/-/issues). PRs, ideas, and feedback are welcome! - **Community** discussions and roadmap live in [GitLab issues](https://git.nationtech.io/nationtech/harmony/-/issues). PRs, ideas, and feedback are welcome!
--- ---
@ -148,4 +147,4 @@ See [LICENSE](LICENSE) for the full text.
--- ---
*Made with ❤️ & 🦀 by the NationTech and the Harmony community* _Made with ❤️ & 🦀 by the NationTech and the Harmony community_

View File

@ -1,20 +1,21 @@
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::dummy::{ErrorScore, PanicScore, SuccessScore}, modules::dummy::{ErrorScore, PanicScore, SuccessScore},
topology::LocalhostTopology, topology::LocalhostTopology,
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let inventory = Inventory::autoload(); harmony_cli::run(
let topology = LocalhostTopology::new(); Inventory::autoload(),
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); LocalhostTopology::new(),
vec![
maestro.register_all(vec![
Box::new(SuccessScore {}), Box::new(SuccessScore {}),
Box::new(ErrorScore {}), Box::new(ErrorScore {}),
Box::new(PanicScore {}), Box::new(PanicScore {}),
]); ],
harmony_cli::init(maestro, None).await.unwrap(); None,
)
.await
.unwrap();
} }

View File

@ -1,7 +1,6 @@
use harmony::{ use harmony::{
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::lamp::{LAMPConfig, LAMPScore}, modules::lamp::{LAMPConfig, LAMPScore},
topology::{K8sAnywhereTopology, Url}, topology::{K8sAnywhereTopology, Url},
}; };
@ -43,15 +42,13 @@ async fn main() {
// K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy // K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy
// locally, to development environment from a CI, to staging, and to production with settings // locally, to development environment from a CI, to staging, and to production with settings
// that automatically adapt to each environment grade. // that automatically adapt to each environment grade.
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize( harmony_cli::run(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(lamp_stack)],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(lamp_stack)]);
// Here we bootstrap the CLI, this gives some nice features if you need them
harmony_cli::init(maestro, None).await.unwrap();
} }
// That's it, end of the infra as code. // That's it, end of the infra as code.

View File

@ -2,7 +2,6 @@ use std::collections::HashMap;
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::{ modules::{
monitoring::{ monitoring::{
alert_channel::discord_alert_channel::DiscordWebhook, alert_channel::discord_alert_channel::DiscordWebhook,
@ -74,13 +73,13 @@ async fn main() {
rules: vec![Box::new(additional_rules), Box::new(additional_rules2)], rules: vec![Box::new(additional_rules), Box::new(additional_rules2)],
service_monitors: vec![service_monitor], service_monitors: vec![service_monitor],
}; };
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
harmony_cli::run(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(alerting_score)],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(alerting_score)]);
harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@ -3,7 +3,6 @@ use std::collections::HashMap;
use harmony::{ use harmony::{
data::Id, data::Id,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::{ modules::{
monitoring::{ monitoring::{
alert_channel::discord_alert_channel::DiscordWebhook, alert_channel::discord_alert_channel::DiscordWebhook,
@ -78,13 +77,13 @@ async fn main() {
rules: vec![Box::new(additional_rules)], rules: vec![Box::new(additional_rules)],
service_monitors: vec![service_monitor], service_monitors: vec![service_monitor],
}; };
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
harmony_cli::run(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(tenant), Box::new(alerting_score)],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(tenant), Box::new(alerting_score)]);
harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@ -1,20 +1,18 @@
use harmony::{ use harmony::{
inventory::Inventory, maestro::Maestro, modules::monitoring::ntfy::ntfy::NtfyScore, inventory::Inventory, modules::monitoring::ntfy::ntfy::NtfyScore, topology::K8sAnywhereTopology,
topology::K8sAnywhereTopology,
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize( harmony_cli::run(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(NtfyScore {
namespace: "monitoring".to_string(),
host: "localhost".to_string(),
})],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(NtfyScore {
namespace: "monitoring".to_string(),
host: "localhost".to_string(),
})]);
harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@ -2,24 +2,15 @@ use std::{path::PathBuf, sync::Arc};
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::application::{ modules::application::{
ApplicationScore, RustWebFramework, RustWebapp, ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring}, features::{ContinuousDelivery, Monitoring},
}, },
topology::{K8sAnywhereTopology, Url}, topology::{K8sAnywhereTopology, Url},
}; };
use harmony_cli::cli_logger;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let cli_logger_handle = cli_logger::init();
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
let application = Arc::new(RustWebapp { let application = Arc::new(RustWebapp {
name: "harmony-example-rust-webapp".to_string(), name: "harmony-example-rust-webapp".to_string(),
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
@ -39,8 +30,12 @@ async fn main() {
application, application,
}; };
maestro.register_all(vec![Box::new(app)]); harmony_cli::run(
harmony_cli::init(maestro, None).await.unwrap(); Inventory::autoload(),
K8sAnywhereTopology::from_env(),
let _ = tokio::try_join!(cli_logger_handle); vec![Box::new(app)],
None,
)
.await
.unwrap();
} }

View File

@ -1,7 +1,6 @@
use harmony::{ use harmony::{
data::Id, data::Id,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::tenant::TenantScore, modules::tenant::TenantScore,
topology::{K8sAnywhereTopology, tenant::TenantConfig}, topology::{K8sAnywhereTopology, tenant::TenantConfig},
}; };
@ -16,15 +15,14 @@ async fn main() {
}, },
}; };
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize( harmony_cli::run(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(tenant)],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(tenant)]);
harmony_cli::init(maestro, None).await.unwrap();
} }
// TODO write tests // TODO write tests

View File

@ -1,6 +1,6 @@
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use log::{debug, info, warn}; use log::{debug, warn};
use crate::instrumentation::{self, HarmonyEvent}; use crate::instrumentation::{self, HarmonyEvent};

View File

@ -1,8 +1,10 @@
use clap::Parser; use clap::Parser;
use clap::builder::ArgPredicate; use clap::builder::ArgPredicate;
use harmony; use harmony::inventory::Inventory;
use harmony::maestro::Maestro;
use harmony::{score::Score, topology::Topology}; use harmony::{score::Score, topology::Topology};
use inquire::Confirm; use inquire::Confirm;
use log::debug;
pub mod cli_logger; // FIXME: Don't make me pub pub mod cli_logger; // FIXME: Don't make me pub
pub mod progress; pub mod progress;
@ -10,7 +12,6 @@ pub mod theme;
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
use harmony_tui; use harmony_tui;
use log::debug;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@ -85,7 +86,24 @@ fn list_scores_with_index<T: Topology>(scores_vec: &Vec<Box<dyn Score<T>>>) -> S
return display_str; return display_str;
} }
pub async fn init<T: Topology + Send + Sync + 'static>( pub async fn run<T: Topology + Send + Sync + 'static>(
inventory: Inventory,
topology: T,
scores: Vec<Box<dyn Score<T>>>,
args_struct: Option<Args>,
) -> Result<(), Box<dyn std::error::Error>> {
let cli_logger_handle = cli_logger::init();
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
maestro.register_all(scores);
let result = init(maestro, args_struct).await;
let _ = tokio::try_join!(cli_logger_handle);
result
}
async fn init<T: Topology + Send + Sync + 'static>(
maestro: harmony::maestro::Maestro<T>, maestro: harmony::maestro::Maestro<T>,
args_struct: Option<Args>, args_struct: Option<Args>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {