Compare commits

...

6 Commits

Author SHA1 Message Date
Ian Letourneau
5cc93d3107 use new harmony_cli::run
All checks were successful
Run Check Script / check (pull_request) Successful in -44s
2025-08-04 17:22:08 -04:00
Ian Letourneau
569839bf66 Merge branch 'master' into feat/crd-alertmanager-configs 2025-08-04 17:04:50 -04:00
54990cd1a5 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
2025-08-04 20:59:07 +00:00
06aab1f57f fix(cli): reduce noise & better track progress within Harmony (#91)
All checks were successful
Run Check Script / check (push) Successful in -37s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 9m6s
Introduce a way to instrument what happens within Harmony and around Harmony (e.g. in the CLI or in Composer).

The goal is to provide visual feedback to the end users and inform them of the progress of their tasks (e.g. deployment) as clearly as possible. It is important to also let them know of the outcome of their tasks (what was created, where to access stuff, etc.).

<img src="https://media.discordapp.net/attachments/1295353830300713062/1400289618636574741/demo.gif?ex=688c18d5&is=688ac755&hm=2c70884aacb08f7bd15cbb65a7562a174846906718aa15294bbb238e64febbce&=" />

## Changes

### Instrumentation architecture
Extensibility and ease of use is key here, while preserving type safety as much as possible.

The proposed API is quite simple:
```rs
// Emit an event
instrumentation::instrument(
    HarmonyEvent::TopologyPrepared {
        topology: "k8s-anywhere",
        outcome: Outcome::success("yay")
    }
);

// Consume events
instrumentation::subscribe("Harmony CLI Logger", async |event| {
    match event {
        HarmonyEvent::TopologyPrepared { name, outcome } => todo!(),
    }
});
```

#### Current limitations
* this API is not very extensible, but it could be easily changed to allow end users to define custom events in addition to Harmony core events
* we use a tokio broadcast channel behind the scene so only in process communication can happen, but it could be easily changed to a more flexible communication mechanism as implementation details are hidden

### `harmony_composer` VS `harmony_cli`
As Harmony Composer launches commands from Harmony (CLI), they both live in different processes. And because of this, we cannot easily make all the logging happens in one place (Harmony Composer) and get rid of Harmony CLI. At least not without introducing additional complexity such as communication through a server, unix socket, etc.

So for the time being, it was decided to preserve both `harmony_composer` and `harmony_cli` and let them independently log their stuff and handle their own responsibilities:
* `harmony_composer`: takes care only of setting up & packaging a project, delegates everything else to `harmony_cli`
* `harmony_cli`: takes care of configuring & running Harmony

### Logging & prompts
* [indicatif](https://github.com/console-rs/indicatif) is used to create progress bars and track progress within Harmony, Harmony CLI, and Harmony Composer
* [inquire](https://github.com/mikaelmello/inquire) is preserved, but was removed from `harmony` (core) as UI concerns shouldn't go that deep
  * note: for now the only prompt we had was simply deleted, we'll have to find a better way to prompt stuff in the future

## Todos
* [ ] Update/Create ADRs
* [ ] Continue instrumentation for missing branches
* [ ] Allow instrumentation to emit and subscribe to custom events

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/91
Reviewed-by: johnride <jg@nationtech.io>
2025-07-31 19:35:33 +00:00
1ab66af718 Merge pull request 'refactor(topo/k8s_anywhere): simplify local installation of k3d' (#90) from simply-k3d-installation into master
Some checks failed
Run Check Script / check (push) Failing after -1m14s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 3m23s
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/90
Reviewed-by: wjro <wrolleman@nationtech.io>
2025-07-31 13:22:25 +00:00
Ian Letourneau
0fff4ef566 refactor(topo/k8s_anywhere): simplify local installation of k3d
All checks were successful
Run Check Script / check (pull_request) Successful in -37s
A Maestro was initialized with a new inventory simply to provide a
localhost topology to install K3D locally. But in practice, the K3D
installation wasn't actually using the topology nor the inventory.

Directly installing K3D within the K8s Anywhere topology makes things
simpler and actually enforce the topology to provide the capabilities
required to install K3D.
2025-07-27 11:50:48 -04:00
29 changed files with 764 additions and 203 deletions

61
Cargo.lock generated
View File

@ -656,6 +656,19 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.0",
"windows-sys 0.60.2",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@ -1143,6 +1156,12 @@ dependencies = [
"serde",
]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "encoding_rs"
version = "0.8.35"
@ -1775,7 +1794,6 @@ dependencies = [
"helm-wrapper-rs",
"hex",
"http 1.3.1",
"inquire",
"k3d-rs",
"k8s-openapi",
"kube",
@ -1784,6 +1802,7 @@ dependencies = [
"libredfish",
"log",
"non-blank-string-rs",
"once_cell",
"opnsense-config",
"opnsense-config-xml",
"pretty_assertions",
@ -1816,10 +1835,15 @@ version = "0.1.0"
dependencies = [
"assert_cmd",
"clap",
"console",
"env_logger",
"harmony",
"harmony_tui",
"indicatif",
"indicatif-log-bridge",
"inquire",
"lazy_static",
"log",
"tokio",
]
@ -1830,10 +1854,16 @@ dependencies = [
"bollard",
"cargo_metadata",
"clap",
"console",
"current_platform",
"env_logger",
"futures-util",
"harmony_cli",
"indicatif",
"indicatif-log-bridge",
"lazy_static",
"log",
"once_cell",
"serde_json",
"tokio",
]
@ -2429,6 +2459,29 @@ dependencies = [
"serde",
]
[[package]]
name = "indicatif"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
"console",
"portable-atomic",
"unicode-width 0.2.0",
"unit-prefix",
"web-time",
]
[[package]]
name = "indicatif-log-bridge"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63703cf9069b85dbe6fe26e1c5230d013dee99d3559cd3d02ba39e099ef7ab02"
dependencies = [
"indicatif",
"log",
]
[[package]]
name = "indoc"
version = "2.0.6"
@ -5261,6 +5314,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unit-prefix"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
[[package]]
name = "universal-hash"
version = "0.5.1"

View File

@ -1,5 +1,6 @@
# Harmony : Open-source infrastructure orchestration that treats your platform like first-class code.
*By [NationTech](https://nationtech.io)*
# Harmony : Open-source infrastructure orchestration that treats your platform like first-class code
_By [NationTech](https://nationtech.io)_
[![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)
@ -23,11 +24,11 @@ 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.
| 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. |
| **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. |
| 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. |
| **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. |
These principles surface as simple, ergonomic Rust APIs that let teams focus on their product while trusting the platform underneath.
@ -63,22 +64,20 @@ async fn main() {
},
};
// 2. Pick where it should run
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, …)
// 2. Enhance with extra scores (monitoring, CI/CD, …)
let mut monitoring = MonitoringAlertingStackScore::new();
monitoring.namespace = Some(lamp_stack.config.namespace.clone());
maestro.register_all(vec![Box::new(lamp_stack), Box::new(monitoring)]);
// 4. Launch an interactive CLI / TUI
harmony_cli::init(maestro, None).await.unwrap();
// 3. Run your scores on the desired topology & inventory
harmony_cli::run(
Inventory::autoload(), // auto-detect hardware / kube-config
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod…
vec![
Box::new(lamp_stack),
Box::new(monitoring)
],
None
).await.unwrap();
}
```
@ -94,13 +93,13 @@ Harmony analyses the code, shows an execution plan in a TUI, and applies it once
## 3 · Core Concepts
| Term | One-liner |
|------|-----------|
| **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). |
| **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, …). |
| **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. |
| Term | One-liner |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). |
| **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, …). |
| **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. |
A visual overview is in the diagram below.
@ -112,9 +111,9 @@ A visual overview is in the diagram below.
Prerequisites:
* Rust
* Docker (if you deploy locally)
* `kubectl` / `helm` for Kubernetes-based topologies
- Rust
- Docker (if you deploy locally)
- `kubectl` / `helm` for Kubernetes-based topologies
```bash
git clone https://git.nationtech.io/nationtech/harmony
@ -126,15 +125,15 @@ cargo build --release # builds the CLI, TUI and libraries
## 5 · Learning More
* **Architectural Decision Records** dive into the rationale
- [ADR-001 · Why Rust](adr/001-rust.md)
- [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md)
- [ADR-006 · Secret Management](adr/006-secret-management.md)
- **Architectural Decision Records** dive into the rationale
- [ADR-001 · Why Rust](adr/001-rust.md)
- [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md)
- [ADR-006 · Secret Management](adr/006-secret-management.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

@ -19,12 +19,6 @@ use harmony::{
#[tokio::main]
async fn main() {
env_logger::init();
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
//TODO there is a bug where the application is deployed into the namespace matching the
//application name and the tenant is created in the namesapce matching the tenant name
//in order for the application to be deployed in the tenant namespace the application.name and
@ -56,6 +50,12 @@ async fn main() {
application,
};
maestro.register_all(vec![Box::new(tenant), Box::new(app)]);
harmony_cli::init(maestro, None).await.unwrap();
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(tenant), Box::new(app)],
None,
)
.await
.unwrap();
}

View File

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

View File

@ -1,7 +1,6 @@
use harmony::{
data::Version,
inventory::Inventory,
maestro::Maestro,
modules::lamp::{LAMPConfig, LAMPScore},
topology::{K8sAnywhereTopology, Url},
};
@ -43,15 +42,13 @@ async fn main() {
// 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
// that automatically adapt to each environment grade.
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(lamp_stack)],
None,
)
.await
.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.

View File

@ -2,7 +2,6 @@ use std::collections::HashMap;
use harmony::{
inventory::Inventory,
maestro::Maestro,
modules::{
monitoring::{
alert_channel::discord_alert_channel::DiscordWebhook,
@ -74,13 +73,13 @@ async fn main() {
rules: vec![Box::new(additional_rules), Box::new(additional_rules2)],
service_monitors: vec![service_monitor],
};
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(alerting_score)],
None,
)
.await
.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::{
data::Id,
inventory::Inventory,
maestro::Maestro,
modules::{
monitoring::{
alert_channel::discord_alert_channel::DiscordWebhook,
@ -78,13 +77,13 @@ async fn main() {
rules: vec![Box::new(additional_rules)],
service_monitors: vec![service_monitor],
};
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(tenant), Box::new(alerting_score)],
None,
)
.await
.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::{
inventory::Inventory, maestro::Maestro, modules::monitoring::ntfy::ntfy::NtfyScore,
topology::K8sAnywhereTopology,
inventory::Inventory, modules::monitoring::ntfy::ntfy::NtfyScore, topology::K8sAnywhereTopology,
};
#[tokio::main]
async fn main() {
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(NtfyScore {
namespace: "monitoring".to_string(),
host: "localhost".to_string(),
})],
None,
)
.await
.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,7 +2,6 @@ use std::{path::PathBuf, sync::Arc};
use harmony::{
inventory::Inventory,
maestro::Maestro,
modules::{
application::{
ApplicationScore, RustWebFramework, RustWebapp,
@ -17,13 +16,6 @@ use harmony::{
#[tokio::main]
async fn main() {
env_logger::init();
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
let application = Arc::new(RustWebapp {
name: "harmony-example-rust-webapp".to_string(),
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
@ -55,6 +47,12 @@ async fn main() {
application,
};
maestro.register_all(vec![Box::new(app)]);
harmony_cli::init(maestro, None).await.unwrap();
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(app)],
None,
)
.await
.unwrap();
}

View File

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

View File

@ -32,7 +32,6 @@ k8s-openapi.workspace = true
serde_yaml.workspace = true
http.workspace = true
serde-value.workspace = true
inquire.workspace = true
helm-wrapper-rs = "0.4.0"
non-blank-string-rs = "1.0.4"
k3d-rs = { path = "../k3d" }
@ -44,12 +43,12 @@ convert_case.workspace = true
email_address = "0.2.9"
chrono.workspace = true
fqdn = { version = "0.4.6", features = [
"domain-label-cannot-start-or-end-with-hyphen",
"domain-label-length-limited-to-63",
"domain-name-without-special-chars",
"domain-name-length-limited-to-255",
"punycode",
"serde",
"domain-label-cannot-start-or-end-with-hyphen",
"domain-label-length-limited-to-63",
"domain-name-without-special-chars",
"domain-name-length-limited-to-255",
"punycode",
"serde",
] }
temp-dir = "0.1.14"
dyn-clone = "1.0.19"
@ -64,6 +63,7 @@ kube-derive = "1.1.0"
bollard.workspace = true
tar.workspace = true
base64.workspace = true
once_cell = "1.21.3"
[dev-dependencies]
pretty_assertions.workspace = true

View File

@ -0,0 +1,63 @@
use log::debug;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
use super::interpret::{InterpretError, Outcome};
#[derive(Debug, Clone)]
pub enum HarmonyEvent {
HarmonyStarted,
PrepareTopologyStarted {
topology: String,
},
TopologyPrepared {
topology: String,
outcome: Outcome,
},
InterpretExecutionStarted {
topology: String,
interpret: String,
message: String,
},
InterpretExecutionFinished {
topology: String,
interpret: String,
outcome: Result<Outcome, InterpretError>,
},
}
static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| {
// TODO: Adjust channel capacity
let (tx, _rx) = broadcast::channel(100);
tx
});
pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> {
match HARMONY_EVENT_BUS.send(event) {
Ok(_) => Ok(()),
Err(_) => Err("send error: no subscribers"),
}
}
pub async fn subscribe<F, Fut>(name: &str, mut handler: F)
where
F: FnMut(HarmonyEvent) -> Fut + Send + 'static,
Fut: Future<Output = bool> + Send,
{
let mut rx = HARMONY_EVENT_BUS.subscribe();
debug!("[{name}] Service started. Listening for events...");
loop {
match rx.recv().await {
Ok(event) => {
if !handler(event).await {
debug!("[{name}] Handler requested exit.");
break;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
debug!("[{name}] Lagged behind by {n} messages.");
}
Err(_) => break,
}
}
}

View File

@ -1,6 +1,8 @@
use std::sync::{Arc, Mutex, RwLock};
use log::{info, warn};
use log::{debug, warn};
use crate::instrumentation::{self, HarmonyEvent};
use super::{
interpret::{InterpretError, InterpretStatus, Outcome},
@ -40,13 +42,18 @@ impl<T: Topology> Maestro<T> {
/// Ensures the associated Topology is ready for operations.
/// Delegates the readiness check and potential setup actions to the Topology.
pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> {
info!("Ensuring topology '{}' is ready...", self.topology.name());
instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted {
topology: self.topology.name().to_string(),
})
.unwrap();
let outcome = self.topology.ensure_ready().await?;
info!(
"Topology '{}' readiness check complete: {}",
self.topology.name(),
outcome.status
);
instrumentation::instrument(HarmonyEvent::TopologyPrepared {
topology: self.topology.name().to_string(),
outcome: outcome.clone(),
})
.unwrap();
self.topology_preparation_result
.lock()
@ -80,11 +87,11 @@ impl<T: Topology> Maestro<T> {
self.topology.name(),
);
}
info!("Running score {score:?}");
debug!("Running score {score:?}");
let interpret = score.create_interpret();
info!("Launching interpret {interpret:?}");
debug!("Launching interpret {interpret:?}");
let result = interpret.execute(&self.inventory, &self.topology).await;
info!("Got result {result:?}");
debug!("Got result {result:?}");
result
}

View File

@ -3,6 +3,7 @@ pub mod data;
pub mod executors;
pub mod filter;
pub mod hardware;
pub mod instrumentation;
pub mod interpret;
pub mod inventory;
pub mod maestro;

View File

@ -1,7 +1,6 @@
use std::{process::Command, sync::Arc};
use async_trait::async_trait;
use inquire::Confirm;
use log::{debug, info, warn};
use serde::Serialize;
use tokio::sync::OnceCell;
@ -10,7 +9,6 @@ use crate::{
executors::ExecutorError,
interpret::{InterpretError, InterpretStatus, Outcome},
inventory::Inventory,
maestro::Maestro,
modules::{
k3d::K3DInstallationScore,
monitoring::kube_prometheus::crd::{
@ -23,7 +21,6 @@ use crate::{
},
},
score::Score,
topology::LocalhostTopology,
};
use super::{
@ -143,9 +140,8 @@ impl K8sAnywhereTopology {
return Err("Failed to run 'helm -version'".to_string());
}
// Print the version output
let version_output = String::from_utf8_lossy(&version_result.stdout);
println!("Helm version: {}", version_output.trim());
debug!("Helm version: {}", version_output.trim());
Ok(())
}
@ -163,9 +159,10 @@ impl K8sAnywhereTopology {
}
async fn try_install_k3d(&self) -> Result<(), InterpretError> {
let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?;
let k3d_score = self.get_k3d_installation_score();
maestro.interpret(Box::new(k3d_score)).await?;
self.get_k3d_installation_score()
.create_interpret()
.execute(&Inventory::empty(), self)
.await?;
Ok(())
}
@ -175,7 +172,7 @@ impl K8sAnywhereTopology {
// TODO this deserves some refactoring, it is becoming a bit hard to figure out
// be careful when making modifications here
if k8s_anywhere_config.use_local_k3d {
info!("Using local k3d cluster because of use_local_k3d set to true");
debug!("Using local k3d cluster because of use_local_k3d set to true");
} else {
if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
debug!("Loading kubeconfig {kubeconfig}");
@ -207,22 +204,13 @@ impl K8sAnywhereTopology {
}
if !k8s_anywhere_config.autoinstall {
debug!("Autoinstall confirmation prompt");
let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ")
.with_default(false)
.prompt()
.expect("Unexpected prompt error");
debug!("Autoinstall confirmation {confirmation}");
if !confirmation {
warn!(
"Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client"
);
return Ok(None);
}
warn!(
"Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client"
);
return Ok(None);
}
info!("Starting K8sAnywhere installation");
debug!("Starting K8sAnywhere installation");
self.try_install_k3d().await?;
let k3d_score = self.get_k3d_installation_score();
// I feel like having to rely on the k3d_rs crate here is a smell
@ -235,7 +223,7 @@ impl K8sAnywhereTopology {
Ok(client) => K8sState {
client: Arc::new(K8sClient::new(client)),
source: K8sSource::LocalK3d,
message: "Successfully installed K3D cluster and acquired client".to_string(),
message: "K8s client ready".to_string(),
},
Err(_) => todo!(),
};
@ -329,7 +317,7 @@ pub struct K8sAnywhereConfig {
///
/// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/
///
/// Default: false
/// Default: true
pub autoinstall: bool,
/// Whether to use local k3d cluster.
@ -348,7 +336,7 @@ impl K8sAnywhereConfig {
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG")
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
autoinstall: std::env::var("HARMONY_AUTOINSTALL")
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
.map_or_else(|_| true, |v| v.parse().ok().unwrap_or(false)),
// TODO harmony_profile should be managed at a more core level than this
harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else(
|_| "dev".to_string(),

View File

@ -1,7 +1,7 @@
use std::{io::Write, process::Command, sync::Arc};
use async_trait::async_trait;
use log::{error, info};
use log::{debug, error};
use serde_yaml::Value;
use tempfile::NamedTempFile;
@ -63,7 +63,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
error!("TODO hardcoded k3d bin path is wrong");
let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d");
// --- 1. Import the container image into the k3d cluster ---
info!(
debug!(
"Importing image '{}' into k3d cluster 'harmony'",
image_name
);
@ -80,7 +80,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
}
// --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file ---
info!("Retrieving kubeconfig for k3d cluster 'harmony'");
debug!("Retrieving kubeconfig for k3d cluster 'harmony'");
let kubeconfig_output = Command::new(&k3d_bin_path)
.args(["kubeconfig", "get", "harmony"])
.output()
@ -101,7 +101,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap();
// --- 3. Install or upgrade the Helm chart in the cluster ---
info!(
debug!(
"Deploying Helm chart '{}' to namespace '{}'",
chart_url, app_name
);
@ -131,7 +131,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
));
}
info!("Successfully deployed '{}' to local k3d cluster.", app_name);
debug!("Successfully deployed '{}' to local k3d cluster.", app_name);
Ok(())
}
}
@ -145,24 +145,19 @@ impl<
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
let image = self.application.image_name();
// TODO
error!(
"TODO reverse helm chart packaging and docker image build. I put helm package first for faster iterations"
);
// TODO Write CI/CD workflow files
// we can autotedect the CI type using the remote url (default to github action for github
// url, etc..)
// Or ask for it when unknown
let helm_chart = self.application.build_push_helm_package(&image).await?;
info!("Pushed new helm chart {helm_chart}");
debug!("Pushed new helm chart {helm_chart}");
error!("TODO Make building image configurable/skippable if image already exists (prompt)");
let image = self.application.build_push_oci_image().await?;
info!("Pushed new docker image {image}");
debug!("Pushed new docker image {image}");
info!("Installing ContinuousDelivery feature");
debug!("Installing ContinuousDelivery feature");
// TODO this is a temporary hack for demo purposes, the deployment target should be driven
// by the topology only and we should not have to know how to perform tasks like this for
// which the topology should be responsible.
@ -182,7 +177,7 @@ impl<
.await?;
}
target => {
info!("Deploying to target {target:?}");
debug!("Deploying to target {target:?}");
let score = ArgoHelmScore {
namespace: "harmonydemo-staging".to_string(),
openshift: false,

View File

@ -5,7 +5,7 @@ mod rust;
use std::sync::Arc;
pub use feature::*;
use log::info;
use log::debug;
pub use oci::*;
pub use rust::*;
@ -37,7 +37,7 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
topology: &T,
) -> Result<Outcome, InterpretError> {
let app_name = self.application.name();
info!(
debug!(
"Preparing {} features [{}] for application {app_name}",
self.features.len(),
self.features
@ -47,7 +47,7 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
.join(", ")
);
for feature in self.features.iter() {
info!(
debug!(
"Installing feature {} for application {app_name}",
feature.name()
);

View File

@ -10,7 +10,7 @@ use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt;
use log::{debug, error, info};
use log::{debug, error, log_enabled};
use serde::Serialize;
use tar::Archive;
@ -73,19 +73,19 @@ impl Application for RustWebapp {
#[async_trait]
impl HelmPackage for RustWebapp {
async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String> {
info!("Starting Helm chart build and push for '{}'", self.name);
debug!("Starting Helm chart build and push for '{}'", self.name);
// 1. Create the Helm chart files on disk.
let chart_dir = self
.create_helm_chart_files(image_url)
.map_err(|e| format!("Failed to create Helm chart files: {}", e))?;
info!("Successfully created Helm chart files in {:?}", chart_dir);
debug!("Successfully created Helm chart files in {:?}", chart_dir);
// 2. Package the chart into a .tgz archive.
let packaged_chart_path = self
.package_helm_chart(&chart_dir)
.map_err(|e| format!("Failed to package Helm chart: {}", e))?;
info!(
debug!(
"Successfully packaged Helm chart: {}",
packaged_chart_path.to_string_lossy()
);
@ -94,7 +94,7 @@ impl HelmPackage for RustWebapp {
let oci_chart_url = self
.push_helm_chart(&packaged_chart_path)
.map_err(|e| format!("Failed to push Helm chart: {}", e))?;
info!("Successfully pushed Helm chart to: {}", oci_chart_url);
debug!("Successfully pushed Helm chart to: {}", oci_chart_url);
Ok(oci_chart_url)
}
@ -107,20 +107,20 @@ impl OCICompliant for RustWebapp {
async fn build_push_oci_image(&self) -> Result<String, String> {
// This function orchestrates the build and push process.
// It's async to match the trait definition, though the underlying docker commands are blocking.
info!("Starting OCI image build and push for '{}'", self.name);
debug!("Starting OCI image build and push for '{}'", self.name);
// 1. Build the image by calling the synchronous helper function.
let image_tag = self.image_name();
self.build_docker_image(&image_tag)
.await
.map_err(|e| format!("Failed to build Docker image: {}", e))?;
info!("Successfully built Docker image: {}", image_tag);
debug!("Successfully built Docker image: {}", image_tag);
// 2. Push the image to the registry.
self.push_docker_image(&image_tag)
.await
.map_err(|e| format!("Failed to push Docker image: {}", e))?;
info!("Successfully pushed Docker image to: {}", image_tag);
debug!("Successfully pushed Docker image to: {}", image_tag);
Ok(image_tag)
}
@ -159,15 +159,17 @@ impl RustWebapp {
&self,
image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> {
info!("Generating Dockerfile for '{}'", self.name);
debug!("Generating Dockerfile for '{}'", self.name);
let _dockerfile_path = self.build_dockerfile()?;
let docker = Docker::connect_with_socket_defaults().unwrap();
let quiet = !log_enabled!(log::Level::Debug);
let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
.dockerfile("Dockerfile.harmony")
.t(image_name)
.q(false)
.q(quiet)
.version(bollard::query_parameters::BuilderVersion::BuilderV1)
.platform("linux/x86_64");
@ -204,7 +206,7 @@ impl RustWebapp {
&self,
image_tag: &str,
) -> Result<String, Box<dyn std::error::Error>> {
info!("Pushing docker image {image_tag}");
debug!("Pushing docker image {image_tag}");
let docker = Docker::connect_with_socket_defaults().unwrap();
@ -533,7 +535,7 @@ spec:
chart_dir: &PathBuf,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let chart_dirname = chart_dir.file_name().expect("Should find a chart dirname");
info!(
debug!(
"Launching `helm package {}` cli with CWD {}",
chart_dirname.to_string_lossy(),
&self
@ -578,7 +580,7 @@ spec:
let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT);
let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name);
info!(
debug!(
"Pushing Helm chart {} to {}",
packaged_chart_path.to_string_lossy(),
oci_push_url

View File

@ -1,12 +1,13 @@
use std::path::PathBuf;
use async_trait::async_trait;
use log::info;
use log::{debug, info};
use serde::Serialize;
use crate::{
config::HARMONY_DATA_DIR,
data::{Id, Version},
instrumentation::{self, HarmonyEvent},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
score::Score,
@ -50,22 +51,38 @@ impl<T: Topology> Interpret<T> for K3dInstallationInterpret {
async fn execute(
&self,
_inventory: &Inventory,
_topology: &T,
topology: &T,
) -> Result<Outcome, InterpretError> {
instrumentation::instrument(HarmonyEvent::InterpretExecutionStarted {
topology: topology.name().into(),
interpret: "k3d-installation".into(),
message: "installing k3d...".into(),
})
.unwrap();
let k3d = k3d_rs::K3d::new(
self.score.installation_path.clone(),
Some(self.score.cluster_name.clone()),
);
match k3d.ensure_installed().await {
let outcome = match k3d.ensure_installed().await {
Ok(_client) => {
let msg = format!("k3d cluster {} is installed ", self.score.cluster_name);
info!("{msg}");
let msg = format!("k3d cluster '{}' installed ", self.score.cluster_name);
debug!("{msg}");
Ok(Outcome::success(msg))
}
Err(msg) => Err(InterpretError::new(format!(
"K3dInstallationInterpret failed to ensure k3d is installed : {msg}"
"failed to ensure k3d is installed : {msg}"
))),
}
};
instrumentation::instrument(HarmonyEvent::InterpretExecutionFinished {
topology: topology.name().into(),
interpret: "k3d-installation".into(),
outcome: outcome.clone(),
})
.unwrap();
outcome
}
fn get_name(&self) -> InterpretName {
InterpretName::K3dInstallation

View File

@ -13,6 +13,11 @@ harmony_tui = { path = "../harmony_tui", optional = true }
inquire.workspace = true
tokio.workspace = true
env_logger.workspace = true
console = "0.16.0"
indicatif = "0.18.0"
lazy_static = "1.5.0"
log.workspace = true
indicatif-log-bridge = "0.2.3"
[features]

View File

@ -0,0 +1,116 @@
use harmony::instrumentation::{self, HarmonyEvent};
use indicatif::{MultiProgress, ProgressBar};
use indicatif_log_bridge::LogWrapper;
use std::{
collections::{HashMap, hash_map},
sync::{Arc, Mutex},
};
use crate::progress;
pub fn init() -> tokio::task::JoinHandle<()> {
configure_logger();
let handle = tokio::spawn(handle_events());
loop {
if instrumentation::instrument(HarmonyEvent::HarmonyStarted).is_ok() {
break;
}
}
handle
}
fn configure_logger() {
let logger =
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build();
let level = logger.filter();
let multi = MultiProgress::new();
LogWrapper::new(multi.clone(), logger).try_init().unwrap();
log::set_max_level(level);
}
async fn handle_events() {
instrumentation::subscribe("Harmony CLI Logger", {
let sections: Arc<Mutex<HashMap<String, MultiProgress>>> =
Arc::new(Mutex::new(HashMap::new()));
let progress_bars: Arc<Mutex<HashMap<String, ProgressBar>>> =
Arc::new(Mutex::new(HashMap::new()));
move |event| {
let sections_clone = Arc::clone(&sections);
let progress_bars_clone = Arc::clone(&progress_bars);
async move {
let mut sections = sections_clone.lock().unwrap();
let mut progress_bars = progress_bars_clone.lock().unwrap();
match event {
HarmonyEvent::HarmonyStarted => {}
HarmonyEvent::PrepareTopologyStarted { topology: name } => {
let section = progress::new_section(format!(
"{} Preparing environment: {name}...",
crate::theme::EMOJI_TOPOLOGY,
));
(*sections).insert(name, section);
}
HarmonyEvent::TopologyPrepared {
topology: name,
outcome,
} => {
let section = (*sections).get(&name).unwrap();
let progress = progress::add_spinner(section, "".into());
match outcome.status {
harmony::interpret::InterpretStatus::SUCCESS => {
progress::success(section, Some(progress), outcome.message);
}
harmony::interpret::InterpretStatus::FAILURE => {
progress::error(section, Some(progress), outcome.message);
}
harmony::interpret::InterpretStatus::RUNNING => todo!(),
harmony::interpret::InterpretStatus::QUEUED => todo!(),
harmony::interpret::InterpretStatus::BLOCKED => todo!(),
harmony::interpret::InterpretStatus::NOOP => {
progress::skip(section, Some(progress), outcome.message);
}
}
}
HarmonyEvent::InterpretExecutionStarted {
interpret: name,
topology,
message,
} => {
let section = (*sections).get(&topology).unwrap();
let progress_bar = progress::add_spinner(section, message);
(*progress_bars).insert(name, progress_bar);
}
HarmonyEvent::InterpretExecutionFinished {
topology,
interpret: name,
outcome,
} => {
let section = (*sections).get(&topology).unwrap();
let progress_bar = (*progress_bars).get(&name).cloned();
let _ = section.clear();
match outcome {
Ok(outcome) => {
progress::success(section, progress_bar, outcome.message);
}
Err(err) => {
progress::error(section, progress_bar, err.to_string());
}
}
(*progress_bars).remove(&name);
}
}
true
}
}
})
.await;
}

View File

@ -1,8 +1,14 @@
use clap::Parser;
use clap::builder::ArgPredicate;
use harmony;
use harmony::inventory::Inventory;
use harmony::maestro::Maestro;
use harmony::{score::Score, topology::Topology};
use inquire::Confirm;
use log::debug;
pub mod cli_logger; // FIXME: Don't make me pub
pub mod progress;
pub mod theme;
#[cfg(feature = "tui")]
use harmony_tui;
@ -80,7 +86,24 @@ fn list_scores_with_index<T: Topology>(scores_vec: &Vec<Box<dyn Score<T>>>) -> S
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>,
args_struct: Option<Args>,
) -> Result<(), Box<dyn std::error::Error>> {
@ -134,7 +157,7 @@ pub async fn init<T: Topology + Send + Sync + 'static>(
// Run filtered scores
for s in scores_vec {
println!("Running: {}", s.name());
debug!("Running: {}", s.name());
maestro.interpret(s).await?;
}

View File

@ -0,0 +1,50 @@
use std::time::Duration;
use indicatif::{MultiProgress, ProgressBar};
pub fn new_section(title: String) -> MultiProgress {
let multi_progress = MultiProgress::new();
let _ = multi_progress.println(title);
multi_progress
}
pub fn add_spinner(multi_progress: &MultiProgress, message: String) -> ProgressBar {
let progress = multi_progress.add(ProgressBar::new_spinner());
progress.set_style(crate::theme::SPINNER_STYLE.clone());
progress.set_message(message);
progress.enable_steady_tick(Duration::from_millis(100));
progress
}
pub fn success(multi_progress: &MultiProgress, progress: Option<ProgressBar>, message: String) {
if let Some(progress) = progress {
multi_progress.remove(&progress)
}
let progress = multi_progress.add(ProgressBar::new_spinner());
progress.set_style(crate::theme::SUCCESS_SPINNER_STYLE.clone());
progress.finish_with_message(message);
}
pub fn error(multi_progress: &MultiProgress, progress: Option<ProgressBar>, message: String) {
if let Some(progress) = progress {
multi_progress.remove(&progress)
}
let progress = multi_progress.add(ProgressBar::new_spinner());
progress.set_style(crate::theme::ERROR_SPINNER_STYLE.clone());
progress.finish_with_message(message);
}
pub fn skip(multi_progress: &MultiProgress, progress: Option<ProgressBar>, message: String) {
if let Some(progress) = progress {
multi_progress.remove(&progress)
}
let progress = multi_progress.add(ProgressBar::new_spinner());
progress.set_style(crate::theme::SKIP_SPINNER_STYLE.clone());
progress.finish_with_message(message);
}

26
harmony_cli/src/theme.rs Normal file
View File

@ -0,0 +1,26 @@
use console::Emoji;
use indicatif::ProgressStyle;
use lazy_static::lazy_static;
pub static EMOJI_HARMONY: Emoji<'_, '_> = Emoji("🎼", "");
pub static EMOJI_SUCCESS: Emoji<'_, '_> = Emoji("", "");
pub static EMOJI_SKIP: Emoji<'_, '_> = Emoji("⏭️", "");
pub static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", "");
pub static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", "");
pub static EMOJI_TOPOLOGY: Emoji<'_, '_> = Emoji("📦", "");
lazy_static! {
pub static ref SPINNER_STYLE: ProgressStyle = ProgressStyle::default_spinner()
.template(" {spinner:.green} {msg}")
.unwrap()
.tick_strings(&["", "", "", "", "", "", "", "", "", ""]);
pub static ref SUCCESS_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE
.clone()
.tick_strings(&[format!("{}", EMOJI_SUCCESS).as_str()]);
pub static ref SKIP_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE
.clone()
.tick_strings(&[format!("{}", EMOJI_SKIP).as_str()]);
pub static ref ERROR_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE
.clone()
.tick_strings(&[format!("{}", EMOJI_ERROR).as_str()]);
}

View File

@ -15,3 +15,9 @@ current_platform = "0.2.0"
futures-util = "0.3.31"
serde_json = "1.0.140"
cargo_metadata = "0.20.0"
indicatif = "0.18.0"
console = "0.16.0"
lazy_static = "1.5.0"
once_cell = "1.21.3"
harmony_cli = { path = "../harmony_cli" }
indicatif-log-bridge = "0.2.3"

View File

@ -0,0 +1,112 @@
use indicatif::{MultiProgress, ProgressBar};
use indicatif_log_bridge::LogWrapper;
use log::error;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use crate::instrumentation::{self, HarmonyComposerEvent};
pub fn init() -> tokio::task::JoinHandle<()> {
configure_logger();
let handle = tokio::spawn(handle_events());
loop {
if instrumentation::instrument(HarmonyComposerEvent::HarmonyComposerStarted).is_ok() {
break;
}
}
handle
}
fn configure_logger() {
let logger =
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build();
let level = logger.filter();
let multi = MultiProgress::new();
LogWrapper::new(multi.clone(), logger).try_init().unwrap();
log::set_max_level(level);
}
pub async fn handle_events() {
const PROGRESS_SETUP: &str = "project-initialization";
const PROGRESS_DEPLOYMENT: &str = "deployment";
instrumentation::subscribe("Harmony Composer Logger", {
let progresses: Arc<Mutex<HashMap<String, MultiProgress>>> =
Arc::new(Mutex::new(HashMap::new()));
let compilation_progress = Arc::new(Mutex::new(None::<ProgressBar>));
move |event| {
let progresses_clone = Arc::clone(&progresses);
let compilation_progress_clone = Arc::clone(&compilation_progress);
async move {
let mut progresses_guard = progresses_clone.lock().unwrap();
let mut compilation_progress_guard = compilation_progress_clone.lock().unwrap();
match event {
HarmonyComposerEvent::HarmonyComposerStarted => {}
HarmonyComposerEvent::ProjectInitializationStarted => {
let multi_progress = harmony_cli::progress::new_section(format!(
"{} Initializing Harmony project...",
harmony_cli::theme::EMOJI_HARMONY,
));
(*progresses_guard).insert(PROGRESS_SETUP.to_string(), multi_progress);
}
HarmonyComposerEvent::ProjectInitialized => println!("\n"),
HarmonyComposerEvent::ProjectCompilationStarted { details } => {
let initialization_progress =
(*progresses_guard).get(PROGRESS_SETUP).unwrap();
let _ = initialization_progress.clear();
let progress =
harmony_cli::progress::add_spinner(initialization_progress, details);
*compilation_progress_guard = Some(progress);
}
HarmonyComposerEvent::ProjectCompiled => {
let initialization_progress =
(*progresses_guard).get(PROGRESS_SETUP).unwrap();
harmony_cli::progress::success(
initialization_progress,
(*compilation_progress_guard).take(),
"project compiled".to_string(),
);
}
HarmonyComposerEvent::ProjectCompilationFailed { details } => {
let initialization_progress =
(*progresses_guard).get(PROGRESS_SETUP).unwrap();
harmony_cli::progress::error(
initialization_progress,
(*compilation_progress_guard).take(),
"failed to compile project".to_string(),
);
error!("{details}");
}
HarmonyComposerEvent::DeploymentStarted { target } => {
let multi_progress = harmony_cli::progress::new_section(format!(
"{} Starting deployment to {target}...\n\n",
harmony_cli::theme::EMOJI_DEPLOY
));
(*progresses_guard).insert(PROGRESS_DEPLOYMENT.to_string(), multi_progress);
}
HarmonyComposerEvent::DeploymentCompleted { details } => println!("\n"),
HarmonyComposerEvent::Shutdown => {
for (_, progresses) in (*progresses_guard).iter() {
progresses.clear().unwrap();
}
return false;
}
}
true
}
}
})
.await
}

View File

@ -0,0 +1,53 @@
use log::debug;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
#[derive(Debug, Clone)]
pub enum HarmonyComposerEvent {
HarmonyComposerStarted,
ProjectInitializationStarted,
ProjectInitialized,
ProjectCompilationStarted { details: String },
ProjectCompiled,
ProjectCompilationFailed { details: String },
DeploymentStarted { target: String },
DeploymentCompleted { details: String },
Shutdown,
}
static HARMONY_COMPOSER_EVENT_BUS: Lazy<broadcast::Sender<HarmonyComposerEvent>> =
Lazy::new(|| {
// TODO: Adjust channel capacity
let (tx, _rx) = broadcast::channel(16);
tx
});
pub fn instrument(event: HarmonyComposerEvent) -> Result<(), &'static str> {
match HARMONY_COMPOSER_EVENT_BUS.send(event) {
Ok(_) => Ok(()),
Err(_) => Err("send error: no subscribers"),
}
}
pub async fn subscribe<F, Fut>(name: &str, mut handler: F)
where
F: FnMut(HarmonyComposerEvent) -> Fut + Send + 'static,
Fut: Future<Output = bool> + Send,
{
let mut rx = HARMONY_COMPOSER_EVENT_BUS.subscribe();
debug!("[{name}] Service started. Listening for events...");
loop {
match rx.recv().await {
Ok(event) => {
if !handler(event).await {
debug!("[{name}] Handler requested exit.");
break;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
debug!("[{name}] Lagged behind by {n} messages.");
}
Err(_) => break,
}
}
}

View File

@ -7,12 +7,16 @@ use bollard::secret::HostConfig;
use cargo_metadata::{Artifact, Message, MetadataCommand};
use clap::{Args, Parser, Subcommand};
use futures_util::StreamExt;
use log::info;
use instrumentation::HarmonyComposerEvent;
use log::{debug, info, log_enabled};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tokio::fs;
mod harmony_composer_logger;
mod instrumentation;
#[derive(Parser)]
#[command(version, about, long_about = None, flatten_help = true, propagate_version = true)]
struct GlobalArgs {
@ -66,13 +70,15 @@ struct AllArgs {
#[tokio::main]
async fn main() {
env_logger::init();
let hc_logger_handle = harmony_composer_logger::init();
let cli_args = GlobalArgs::parse();
let harmony_path = Path::new(&cli_args.harmony_path)
.try_exists()
.expect("couldn't check if path exists");
instrumentation::instrument(HarmonyComposerEvent::ProjectInitializationStarted).unwrap();
let harmony_bin_path: PathBuf = match harmony_path {
true => {
compile_harmony(
@ -85,6 +91,8 @@ async fn main() {
false => todo!("implement autodetect code"),
};
instrumentation::instrument(HarmonyComposerEvent::ProjectInitialized).unwrap();
match cli_args.command {
Some(command) => match command {
Commands::Check(args) => {
@ -116,19 +124,31 @@ async fn main() {
}
Commands::Deploy(args) => {
let deploy = if args.staging {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "staging".to_string(),
})
.unwrap();
todo!("implement staging deployment")
} else if args.prod {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "prod".to_string(),
})
.unwrap();
todo!("implement prod deployment")
} else {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "dev".to_string(),
})
.unwrap();
Command::new(harmony_bin_path).arg("-y").arg("-a").spawn()
}
.expect("failed to run harmony deploy");
let deploy_output = deploy.wait_with_output().unwrap();
println!(
"deploy output: {}",
String::from_utf8(deploy_output.stdout).expect("couldn't parse from utf8")
);
instrumentation::instrument(HarmonyComposerEvent::DeploymentCompleted {
details: String::from_utf8(deploy_output.stdout).unwrap(),
})
.unwrap();
}
Commands::All(_args) => todo!(
"take all previous match arms and turn them into separate functions, and call them all one after the other"
@ -137,6 +157,10 @@ async fn main() {
},
None => todo!("run interactively, ask for info on CLI"),
}
instrumentation::instrument(HarmonyComposerEvent::Shutdown).unwrap();
let _ = tokio::try_join!(hc_logger_handle);
}
#[derive(Clone, Debug, clap::ValueEnum)]
@ -166,17 +190,32 @@ async fn compile_harmony(
Some(m) => m,
None => {
if cargo_exists {
return compile_cargo(platform, harmony_location).await;
CompileMethod::LocalCargo
} else {
return compile_docker(platform, harmony_location).await;
CompileMethod::Docker
}
}
};
match method {
CompileMethod::LocalCargo => return compile_cargo(platform, harmony_location).await,
CompileMethod::Docker => return compile_docker(platform, harmony_location).await,
let path = match method {
CompileMethod::LocalCargo => {
instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted {
details: "compiling project with cargo".to_string(),
})
.unwrap();
compile_cargo(platform, harmony_location).await
}
CompileMethod::Docker => {
instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted {
details: "compiling project with docker".to_string(),
})
.unwrap();
compile_docker(platform, harmony_location).await
}
};
instrumentation::instrument(HarmonyComposerEvent::ProjectCompiled).unwrap();
path
}
// TODO: make sure this works with cargo workspaces
@ -186,6 +225,12 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
.exec()
.unwrap();
let stderr = if log_enabled!(log::Level::Debug) {
Stdio::inherit()
} else {
Stdio::piped()
};
let mut cargo_build = Command::new("cargo")
.current_dir(&harmony_location)
.args(vec![
@ -195,6 +240,7 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
"--message-format=json-render-diagnostics",
])
.stdout(Stdio::piped())
.stderr(stderr)
.spawn()
.expect("run cargo command failed");
@ -210,18 +256,20 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
.expect("failed to get root package")
.manifest_path
{
println!("{:?}", artifact);
debug!("{:?}", artifact);
artifacts.push(artifact);
}
}
Message::BuildScriptExecuted(_script) => (),
Message::BuildFinished(finished) => {
println!("{:?}", finished);
debug!("{:?}", finished);
}
_ => (), // Unknown message
}
}
cargo_build.wait().expect("run cargo command failed");
let bin = artifacts
.last()
.expect("no binaries built")
@ -237,7 +285,8 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
bin_out = PathBuf::from(format!("{}/harmony", harmony_location));
let _copy_res = fs::copy(&bin, &bin_out).await;
}
return bin_out;
bin_out
}
async fn compile_docker(platform: String, harmony_location: String) -> PathBuf {

View File

@ -2,7 +2,7 @@ mod downloadable_asset;
use downloadable_asset::*;
use kube::Client;
use log::{debug, info, warn};
use log::{debug, warn};
use std::path::PathBuf;
const K3D_BIN_FILE_NAME: &str = "k3d";
@ -90,7 +90,7 @@ impl K3d {
let latest_release = self.get_latest_release_tag().await.unwrap();
let release_binary = self.get_binary_for_current_platform(latest_release).await;
info!("Foudn K3d binary to install : {release_binary:#?}");
debug!("Foudn K3d binary to install : {release_binary:#?}");
release_binary.download_to_path(self.base_dir.clone()).await
}
@ -175,7 +175,7 @@ impl K3d {
Err(_) => return Err("Could not get cluster_name, cannot initialize".to_string()),
};
info!("Initializing k3d cluster '{}'", cluster_name);
debug!("Initializing k3d cluster '{}'", cluster_name);
self.create_cluster(cluster_name)?;
self.create_kubernetes_client().await
@ -205,7 +205,7 @@ impl K3d {
/// - `Err(String)` - Error message if any step failed
pub async fn ensure_installed(&self) -> Result<Client, String> {
if !self.is_installed() {
info!("K3d is not installed, downloading latest release");
debug!("K3d is not installed, downloading latest release");
self.download_latest_release()
.await
.map_err(|e| format!("Failed to download k3d: {}", e))?;
@ -216,13 +216,13 @@ impl K3d {
}
if !self.is_cluster_initialized() {
info!("Cluster is not initialized, initializing now");
debug!("Cluster is not initialized, initializing now");
return self.initialize_cluster().await;
}
self.start_cluster().await?;
info!("K3d and cluster are already properly set up");
debug!("K3d and cluster are already properly set up");
self.create_kubernetes_client().await
}
@ -325,12 +325,12 @@ impl K3d {
return Err(format!("Failed to create cluster: {}", stderr));
}
info!("Successfully created k3d cluster '{}'", cluster_name);
debug!("Successfully created k3d cluster '{}'", cluster_name);
Ok(())
}
async fn create_kubernetes_client(&self) -> Result<Client, String> {
warn!("TODO this method is way too dumb, it should make sure that the client is connected to the k3d cluster actually represented by this instance, not just any default client");
// TODO: Connect the client to the right k3d cluster (see https://git.nationtech.io/NationTech/harmony/issues/92)
Client::try_default()
.await
.map_err(|e| format!("Failed to create Kubernetes client: {}", e))
@ -352,7 +352,7 @@ impl K3d {
return Err(format!("Failed to start cluster: {}", stderr));
}
info!("Successfully started k3d cluster '{}'", cluster_name);
debug!("Successfully started k3d cluster '{}'", cluster_name);
Ok(())
}
}