fix(cli): reduce noise & better track progress within Harmony #91

Merged
letian merged 8 commits from better-cli into master 2025-07-31 19:35:36 +00:00
9 changed files with 249 additions and 15 deletions
Showing only changes of commit 6f7e1640c1 - Show all commits

43
Cargo.lock generated
View File

@ -650,6 +650,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"
@ -1137,6 +1150,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"
@ -1765,6 +1784,7 @@ dependencies = [
"libredfish",
"log",
"non-blank-string-rs",
"once_cell",
"opnsense-config",
"opnsense-config-xml",
"pretty_assertions",
@ -1810,9 +1830,13 @@ dependencies = [
"bollard",
"cargo_metadata",
"clap",
"console",
"current_platform",
"env_logger",
"futures-util",
"harmony",
"indicatif",
"lazy_static",
"log",
"serde_json",
"tokio",
@ -2409,6 +2433,19 @@ 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 = "indoc"
version = "2.0.6"
@ -5181,6 +5218,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

@ -62,6 +62,7 @@ serde_with = "3.14.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,51 @@
use log::debug;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
// FIXME: Ce module d'instrumentation ne peut pas fonctionner à la fois pour Harmony Composer et
// Harmony CLI. Elle n'a donc pas entièrement sa place ici et devra être séparée en deux.
#[derive(Debug, Clone)]
pub enum HarmonyEvent {
ProjectInitializationStarted,
ProjectInitialized,
ProjectCompilationStarted { details: String },
ProjectCompiled,
ProjectCompilationFailed { details: String },
DeploymentStarted { target: String },
DeploymentCompleted { details: String },
PrepareTopologyStarted { name: String },
Shutdown,
}
static EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| {
// TODO: Adjust channel capacity
let (tx, _rx) = broadcast::channel(16);
tx
});
pub fn instrument(event: HarmonyEvent) {
EVENT_BUS.send(event).expect("couldn't send event");
}
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 = EVENT_BUS.subscribe();
debug!("[{name}] Service started. Listening for events...");
loop {
match rx.recv().await {
Review

some kind of retry mechanism could be useful here to make sure no events are lost

some kind of retry mechanism could be useful here to make sure no events are lost
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

@ -2,6 +2,8 @@ use std::sync::{Arc, Mutex, RwLock};
use log::{info, warn};
use crate::instrumentation::{self, HarmonyEvent};
use super::{
interpret::{InterpretError, InterpretStatus, Outcome},
inventory::Inventory,
@ -40,7 +42,12 @@ 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());
// FIXME: Cette instrumentation ne peut pas communiquer avec Harmony Composer puisqu'il
// s'agit d'un process différent.
//
// instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted {
// name: self.topology.name().to_string(),
// });
let outcome = self.topology.ensure_ready().await?;
info!(
"Topology '{}' readiness check complete: {}",

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

@ -159,7 +159,7 @@ 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? : ")
let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? :")
.with_default(false)
.prompt()
.expect("Unexpected prompt error");

View File

@ -15,3 +15,7 @@ current_platform = "0.2.0"
futures-util = "0.3.31"
serde_json = "1.0.140"
cargo_metadata = "0.20.0"
harmony = { path = "../harmony" }
indicatif = "0.18.0"
console = "0.16.0"
lazy_static = "1.5.0"

View File

@ -0,0 +1,84 @@
use console::Emoji;
use harmony::instrumentation::{self, HarmonyEvent};
use indicatif::{ProgressBar, ProgressStyle};
use lazy_static::lazy_static;
use log::error;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
static EMOJI_HARMONY: Emoji<'_, '_> = Emoji("🎼", "");
static EMOJI_SUCCESS: Emoji<'_, '_> = Emoji("", "");
static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", "");
static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", "");
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 ERROR_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE
.clone()
.tick_strings(&[format!("{}", EMOJI_ERROR).as_str()]);
}
pub async fn init() {
instrumentation::subscribe("CLI Logger", {
let current_spinner = Arc::new(Mutex::new(None::<ProgressBar>));
move |event| {
let spinner_clone = Arc::clone(&current_spinner);
async move {
let mut spinner_guard = spinner_clone.lock().unwrap();
match event {
HarmonyEvent::ProjectInitializationStarted => {
println!("{} Initializing Harmony project...", EMOJI_HARMONY);
}
HarmonyEvent::ProjectInitialized => println!("\n"),
HarmonyEvent::ProjectCompilationStarted { details } => {
let progress = ProgressBar::new_spinner();
progress.set_style(SPINNER_STYLE.clone());
progress.set_message(details);
progress.enable_steady_tick(Duration::from_millis(100));
*spinner_guard = Some(progress);
}
HarmonyEvent::ProjectCompiled => {
if let Some(progress) = spinner_guard.take() {
progress.set_style(SUCCESS_SPINNER_STYLE.clone());
progress.finish_with_message("project compiled");
}
}
HarmonyEvent::ProjectCompilationFailed { details } => {
if let Some(progress) = spinner_guard.take() {
progress.set_style(ERROR_SPINNER_STYLE.clone());
progress.finish_with_message("failed to compile project");
error!("{details}");
}
}
HarmonyEvent::DeploymentStarted { target } => {
println!("{} Starting deployment to {target}...", EMOJI_DEPLOY);
}
HarmonyEvent::DeploymentCompleted { details } => println!("\n"),
HarmonyEvent::PrepareTopologyStarted { name } => {
println!("{} Preparing environment: {name}...", EMOJI_TOPOLOGY);
}
HarmonyEvent::Shutdown => {
if let Some(progress) = spinner_guard.take() {
progress.abandon();
}
return false;
}
}
true
}
}
})
.await
}

View File

@ -7,12 +7,15 @@ use bollard::secret::HostConfig;
use cargo_metadata::{Artifact, Message, MetadataCommand};
use clap::{Args, Parser, Subcommand};
use futures_util::StreamExt;
use log::info;
use harmony::instrumentation::{self, HarmonyEvent};
use log::{debug, info, log_enabled};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tokio::fs;
mod cli_logger;
#[derive(Parser)]
#[command(version, about, long_about = None, flatten_help = true, propagate_version = true)]
struct GlobalArgs {
@ -67,12 +70,15 @@ struct AllArgs {
#[tokio::main]
async fn main() {
env_logger::init();
let cli_logger_handle = tokio::spawn(cli_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(HarmonyEvent::ProjectInitializationStarted);
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(HarmonyEvent::ProjectInitialized);
match cli_args.command {
Some(command) => match command {
Commands::Check(args) => {
@ -116,19 +124,27 @@ async fn main() {
}
Commands::Deploy(args) => {
let deploy = if args.staging {
instrumentation::instrument(HarmonyEvent::DeploymentStarted {
target: "staging".to_string(),
});
todo!("implement staging deployment")
} else if args.prod {
instrumentation::instrument(HarmonyEvent::DeploymentStarted {
target: "prod".to_string(),
});
todo!("implement prod deployment")
} else {
instrumentation::instrument(HarmonyEvent::DeploymentStarted {
target: "dev".to_string(),
});
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(HarmonyEvent::DeploymentCompleted {
details: String::from_utf8(deploy_output.stdout).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 +153,10 @@ async fn main() {
},
None => todo!("run interactively, ask for info on CLI"),
}
instrumentation::instrument(HarmonyEvent::Shutdown);

Some instrumentations are still missing, but this PR is mostly to get early feedback on the approach.

Some instrumentations are still missing, but this PR is mostly to get early feedback on the approach.
let _ = tokio::try_join!(cli_logger_handle);
}
#[derive(Clone, Debug, clap::ValueEnum)]
@ -166,17 +186,30 @@ 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(HarmonyEvent::ProjectCompilationStarted {
details: "compiling project with cargo".to_string(),
});
compile_cargo(platform, harmony_location).await
}
CompileMethod::Docker => {
instrumentation::instrument(HarmonyEvent::ProjectCompilationStarted {
details: "compiling project with docker".to_string(),
});
compile_docker(platform, harmony_location).await
}
};
instrumentation::instrument(HarmonyEvent::ProjectCompiled);
path
}
// TODO: make sure this works with cargo workspaces
@ -186,6 +219,12 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
.exec()
.unwrap();
let stderr = if log_enabled!(log::Level::Debug) {
Stdio::inherit()

cargo prints warnings and other stuff in stderr, meaning it can get really noisy. So we'll "silence" those warnings unless we're asking for debug logs (RUST_LOG=debug)

`cargo` prints warnings and other stuff in `stderr`, meaning it can get really noisy. So we'll "silence" those warnings unless we're asking for debug logs (`RUST_LOG=debug`)
} else {
Stdio::piped()
};
let mut cargo_build = Command::new("cargo")
.current_dir(&harmony_location)
.args(vec![
@ -195,6 +234,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 +250,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 +279,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 {