fix(cli): reduce noise & better track progress within Harmony
All checks were successful
Run Check Script / check (pull_request) Successful in -35s

This commit is contained in:
Ian Letourneau 2025-07-27 17:41:43 -04:00
parent 0fff4ef566
commit 6f7e1640c1
9 changed files with 249 additions and 15 deletions

43
Cargo.lock generated
View File

@ -650,6 +650,19 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@ -1137,6 +1150,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@ -1765,6 +1784,7 @@ dependencies = [
"libredfish", "libredfish",
"log", "log",
"non-blank-string-rs", "non-blank-string-rs",
"once_cell",
"opnsense-config", "opnsense-config",
"opnsense-config-xml", "opnsense-config-xml",
"pretty_assertions", "pretty_assertions",
@ -1810,9 +1830,13 @@ dependencies = [
"bollard", "bollard",
"cargo_metadata", "cargo_metadata",
"clap", "clap",
"console",
"current_platform", "current_platform",
"env_logger", "env_logger",
"futures-util", "futures-util",
"harmony",
"indicatif",
"lazy_static",
"log", "log",
"serde_json", "serde_json",
"tokio", "tokio",
@ -2409,6 +2433,19 @@ dependencies = [
"serde", "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]] [[package]]
name = "indoc" name = "indoc"
version = "2.0.6" version = "2.0.6"
@ -5181,6 +5218,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unit-prefix"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
[[package]] [[package]]
name = "universal-hash" name = "universal-hash"
version = "0.5.1" version = "0.5.1"

View File

@ -62,6 +62,7 @@ serde_with = "3.14.0"
bollard.workspace = true bollard.workspace = true
tar.workspace = true tar.workspace = true
base64.workspace = true base64.workspace = true
once_cell = "1.21.3"
[dev-dependencies] [dev-dependencies]
pretty_assertions.workspace = true 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 {
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 log::{info, warn};
use crate::instrumentation::{self, HarmonyEvent};
use super::{ use super::{
interpret::{InterpretError, InterpretStatus, Outcome}, interpret::{InterpretError, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
@ -40,7 +42,12 @@ impl<T: Topology> Maestro<T> {
/// Ensures the associated Topology is ready for operations. /// Ensures the associated Topology is ready for operations.
/// Delegates the readiness check and potential setup actions to the Topology. /// Delegates the readiness check and potential setup actions to the Topology.
pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> { 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?; let outcome = self.topology.ensure_ready().await?;
info!( info!(
"Topology '{}' readiness check complete: {}", "Topology '{}' readiness check complete: {}",

View File

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

View File

@ -159,7 +159,7 @@ impl K8sAnywhereTopology {
if !k8s_anywhere_config.autoinstall { if !k8s_anywhere_config.autoinstall {
debug!("Autoinstall confirmation prompt"); 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) .with_default(false)
.prompt() .prompt()
.expect("Unexpected prompt error"); .expect("Unexpected prompt error");

View File

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