From ceafabf430c7acc3a74d0bd831b5ce2aee3955ed Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Tue, 9 Sep 2025 17:59:14 -0400 Subject: [PATCH 1/5] wip: Report harmony execution outcome --- harmony/src/domain/interpret/mod.rs | 21 ++++++ harmony/src/modules/dhcp.rs | 41 +++++----- harmony/src/modules/dns.rs | 17 ++--- harmony/src/modules/helm/chart.rs | 35 ++++----- harmony/src/modules/inventory/mod.rs | 7 +- harmony/src/modules/k8s/ingress.rs | 75 +++++++++++++++++-- .../src/modules/okd/bootstrap_01_prepare.rs | 25 +++---- .../src/modules/okd/bootstrap_02_bootstrap.rs | 28 +++---- .../modules/okd/bootstrap_03_control_plane.rs | 17 ++--- .../src/modules/okd/bootstrap_04_workers.rs | 33 ++------ .../modules/okd/bootstrap_05_sanity_check.rs | 40 +++------- .../okd/bootstrap_06_installation_report.rs | 32 ++------ harmony_cli/src/cli_reporter.rs | 32 ++++++++ harmony_cli/src/lib.rs | 2 + .../src/harmony_composer_logger.rs | 1 - harmony_composer/src/instrumentation.rs | 1 - 16 files changed, 217 insertions(+), 190 deletions(-) create mode 100644 harmony_cli/src/cli_reporter.rs diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index f1abcda..17afa86 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -34,6 +34,7 @@ pub enum InterpretName { CephClusterHealth, Custom(&'static str), RHOBAlerting, + K8sIngress, } impl std::fmt::Display for InterpretName { @@ -64,6 +65,7 @@ impl std::fmt::Display for InterpretName { InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"), InterpretName::Custom(name) => f.write_str(name), InterpretName::RHOBAlerting => f.write_str("RHOBAlerting"), + InterpretName::K8sIngress => f.write_str("K8sIngress"), } } } @@ -82,6 +84,7 @@ pub trait Interpret: std::fmt::Debug + Send { pub struct Outcome { pub status: InterpretStatus, pub message: String, + pub details: Vec, } impl Outcome { @@ -89,6 +92,7 @@ impl Outcome { Self { status: InterpretStatus::NOOP, message: String::new(), + details: vec![], } } @@ -96,6 +100,23 @@ impl Outcome { Self { status: InterpretStatus::SUCCESS, message, + details: vec![], + } + } + + pub fn success_with_details(message: String, details: Vec) -> Self { + Self { + status: InterpretStatus::SUCCESS, + message, + details, + } + } + + pub fn running(message: String) -> Self { + Self { + status: InterpretStatus::RUNNING, + message, + details: vec![], } } } diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index eff2912..e261220 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -69,17 +69,14 @@ impl DhcpInterpret { dhcp_server.set_pxe_options(pxe_options).await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!( - "Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]", - self.score.boot_filename, - self.score.boot_filename, - self.score.filename, - self.score.filename64, - self.score.filenameipxe - ), - )) + Ok(Outcome::success(format!( + "Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]", + self.score.boot_filename, + self.score.boot_filename, + self.score.filename, + self.score.filename64, + self.score.filenameipxe + ))) } } @@ -122,8 +119,7 @@ impl Interpret for DhcpInterpret { topology.commit_config().await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, + Ok(Outcome::success( "Dhcp Interpret execution successful".to_string(), )) } @@ -197,10 +193,10 @@ impl DhcpHostBindingInterpret { } } - Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!("Dhcp Interpret registered {} entries", number_new_entries), - )) + Ok(Outcome::success(format!( + "Dhcp Interpret registered {} entries", + number_new_entries + ))) } } @@ -236,12 +232,9 @@ impl Interpret for DhcpHostBindingInterpret { topology.commit_config().await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!( - "Dhcp Host Binding Interpret execution successful on {} hosts", - self.score.host_binding.len() - ), - )) + Ok(Outcome::success(format!( + "Dhcp Host Binding Interpret execution successful on {} hosts", + self.score.host_binding.len() + ))) } } diff --git a/harmony/src/modules/dns.rs b/harmony/src/modules/dns.rs index 9608fa1..b0d3a1d 100644 --- a/harmony/src/modules/dns.rs +++ b/harmony/src/modules/dns.rs @@ -55,8 +55,7 @@ impl DnsInterpret { dns.register_dhcp_leases(register).await?; } - Ok(Outcome::new( - InterpretStatus::SUCCESS, + Ok(Outcome::success( "DNS Interpret execution successfull".to_string(), )) } @@ -68,13 +67,10 @@ impl DnsInterpret { let entries = &self.score.dns_entries; dns_server.ensure_hosts_registered(entries.clone()).await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!( - "DnsInterpret registered {} hosts successfully", - entries.len() - ), - )) + Ok(Outcome::success(format!( + "DnsInterpret registered {} hosts successfully", + entries.len() + ))) } } @@ -111,8 +107,7 @@ impl Interpret for DnsInterpret { topology.commit_config().await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, + Ok(Outcome::success( "Dns Interpret execution successful".to_string(), )) } diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index 37769d7..e2f8057 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -197,13 +197,10 @@ impl Interpret for HelmChartInterpret { self.score.release_name, ns ); - return Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!( - "Helm Chart '{}' already installed to namespace {ns} and install_only=true", - self.score.release_name - ), - )); + return Ok(Outcome::success(format!( + "Helm Chart '{}' already installed to namespace {ns} and install_only=true", + self.score.release_name + ))); } else { info!( "Release '{}' not found in namespace '{}'. Proceeding with installation.", @@ -228,18 +225,18 @@ impl Interpret for HelmChartInterpret { }; match status { - helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!("Helm Chart {} deployed", self.score.release_name), - )), - helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::new( - InterpretStatus::RUNNING, - format!("Helm Chart {} pending install...", self.score.release_name), - )), - helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::new( - InterpretStatus::RUNNING, - format!("Helm Chart {} pending upgrade...", self.score.release_name), - )), + helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::success(format!( + "Helm Chart {} deployed", + self.score.release_name + ))), + helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::running(format!( + "Helm Chart {} pending install...", + self.score.release_name + ))), + helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::running(format!( + "Helm Chart {} pending upgrade...", + self.score.release_name + ))), helm_wrapper_rs::HelmDeployStatus::Failed => Err(InterpretError::new(format!( "Helm Chart {} installation failed", self.score.release_name diff --git a/harmony/src/modules/inventory/mod.rs b/harmony/src/modules/inventory/mod.rs index 0274dc4..174231b 100644 --- a/harmony/src/modules/inventory/mod.rs +++ b/harmony/src/modules/inventory/mod.rs @@ -133,10 +133,9 @@ impl Interpret for DiscoverInventoryAgentInterpret { }, ) .await; - Ok(Outcome { - status: InterpretStatus::SUCCESS, - message: "Discovery process completed successfully".to_string(), - }) + Ok(Outcome::success( + "Discovery process completed successfully".to_string(), + )) } fn get_name(&self) -> InterpretName { diff --git a/harmony/src/modules/k8s/ingress.rs b/harmony/src/modules/k8s/ingress.rs index eb5478f..d94a1aa 100644 --- a/harmony/src/modules/k8s/ingress.rs +++ b/harmony/src/modules/k8s/ingress.rs @@ -1,11 +1,15 @@ +use async_trait::async_trait; use harmony_macros::ingress_path; +use harmony_types::id::Id; use k8s_openapi::api::networking::v1::Ingress; use log::{debug, trace}; use serde::Serialize; use serde_json::json; use crate::{ - interpret::Interpret, + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, score::Score, topology::{K8sclient, Topology}, }; @@ -57,7 +61,7 @@ impl Score for K8sIngressScore { let ingress_class = match self.ingress_class_name.clone() { Some(ingress_class_name) => ingress_class_name, - None => format!("\"default\""), + None => "\"default\"".to_string(), }; let ingress = json!( @@ -97,11 +101,12 @@ impl Score for K8sIngressScore { "Successfully built Ingress for host {:?}", ingress.metadata.name ); - Box::new(K8sResourceInterpret { - score: K8sResourceScore::single( - ingress.clone(), - self.namespace.clone().map(|f| f.to_string()), - ), + + Box::new(K8sIngressInterpret { + ingress, + service: self.name.to_string(), + namespace: self.namespace.clone().map(|f| f.to_string()), + host: self.host.clone(), }) } @@ -109,3 +114,59 @@ impl Score for K8sIngressScore { format!("{} K8sIngressScore", self.name) } } + +#[derive(std::fmt::Debug)] +struct K8sIngressInterpret { + ingress: Ingress, + service: String, + namespace: Option, + host: fqdn::FQDN, +} + +#[async_trait] +impl Interpret for K8sIngressInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + let result = K8sResourceInterpret { + score: K8sResourceScore::single(self.ingress.clone(), self.namespace.clone()), + } + .execute(inventory, topology) + .await; + + match result { + Ok(outcome) => match outcome.status { + InterpretStatus::SUCCESS => { + let details = match &self.namespace { + Some(namespace) => { + vec![format!("{} ({namespace}): {}", self.service, self.host)] + } + None => vec![format!("{}: {}", self.service, self.host)], + }; + + Ok(Outcome::success_with_details(outcome.message, details)) + } + _ => Ok(outcome), + }, + Err(e) => Err(e), + } + } + + fn get_name(&self) -> InterpretName { + InterpretName::K8sIngress + } + + fn get_version(&self) -> Version { + Version::from("0.0.1").unwrap() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + vec![] + } +} diff --git a/harmony/src/modules/okd/bootstrap_01_prepare.rs b/harmony/src/modules/okd/bootstrap_01_prepare.rs index d3409e2..57b71d9 100644 --- a/harmony/src/modules/okd/bootstrap_01_prepare.rs +++ b/harmony/src/modules/okd/bootstrap_01_prepare.rs @@ -1,19 +1,19 @@ -use async_trait::async_trait; -use derive_new::new; -use harmony_types::id::Id; -use log::{error, info, warn}; -use serde::Serialize; - use crate::{ data::Version, hardware::PhysicalHost, infra::inventory::InventoryRepositoryFactory, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::{HostRole, Inventory}, - modules::inventory::{DiscoverHostForRoleScore, LaunchDiscoverInventoryAgentScore}, + modules::inventory::DiscoverHostForRoleScore, score::Score, topology::HAClusterTopology, }; +use async_trait::async_trait; +use derive_new::new; +use harmony_types::id::Id; +use log::info; +use serde::Serialize; + // ------------------------------------------------------------------------------------------------- // Step 01: Inventory (default PXE + Kickstart in RAM + Rust agent) // - This score exposes/ensures the default inventory assets and waits for discoveries. @@ -109,12 +109,9 @@ When you can dig them, confirm to continue. .await?; } - Ok(Outcome::new( - InterpretStatus::SUCCESS, - format!( - "Found and assigned bootstrap node: {}", - bootstrap_host.unwrap().summary() - ), - )) + Ok(Outcome::success(format!( + "Found and assigned bootstrap node: {}", + bootstrap_host.unwrap().summary() + ))) } } diff --git a/harmony/src/modules/okd/bootstrap_02_bootstrap.rs b/harmony/src/modules/okd/bootstrap_02_bootstrap.rs index 5b940fb..e9b3a6a 100644 --- a/harmony/src/modules/okd/bootstrap_02_bootstrap.rs +++ b/harmony/src/modules/okd/bootstrap_02_bootstrap.rs @@ -1,25 +1,13 @@ -use std::{fmt::Write, path::PathBuf}; - -use async_trait::async_trait; -use derive_new::new; -use harmony_secret::SecretManager; -use harmony_types::id::Id; -use log::{debug, error, info, warn}; -use serde::{Deserialize, Serialize}; -use tokio::{fs::File, io::AsyncWriteExt, process::Command}; - use crate::{ config::secret::{RedhatSecret, SshKeyPair}, data::{FileContent, FilePath, Version}, hardware::PhysicalHost, infra::inventory::InventoryRepositoryFactory, - instrumentation::{HarmonyEvent, instrument}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::{HostRole, Inventory}, modules::{ dhcp::DhcpHostBindingScore, http::{IPxeMacBootFileScore, StaticFilesHttpScore}, - inventory::LaunchDiscoverInventoryAgentScore, okd::{ bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, templates::{BootstrapIpxeTpl, InstallConfigYaml}, @@ -28,6 +16,15 @@ use crate::{ score::Score, topology::{HAClusterTopology, HostBinding}, }; +use async_trait::async_trait; +use derive_new::new; +use harmony_secret::SecretManager; +use harmony_types::id::Id; +use log::{debug, info}; +use serde::Serialize; +use std::path::PathBuf; +use tokio::{fs::File, io::AsyncWriteExt, process::Command}; + // ------------------------------------------------------------------------------------------------- // Step 02: Bootstrap // - Select bootstrap node (from discovered set). @@ -313,7 +310,7 @@ impl OKDSetup02BootstrapInterpret { info!("[Bootstrap] Rebooting bootstrap node via SSH"); // TODO reboot programatically, there are some logical checks and refactoring to do such as // accessing the bootstrap node config (ip address) from the inventory - let confirmation = inquire::Confirm::new( + let _ = inquire::Confirm::new( "Now reboot the bootstrap node so it picks up its pxe boot file. Press enter when ready.", ) .prompt() @@ -379,9 +376,6 @@ impl Interpret for OKDSetup02BootstrapInterpret { self.reboot_target().await?; self.wait_for_bootstrap_complete().await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, - "Bootstrap phase complete".into(), - )) + Ok(Outcome::success("Bootstrap phase complete".into())) } } diff --git a/harmony/src/modules/okd/bootstrap_03_control_plane.rs b/harmony/src/modules/okd/bootstrap_03_control_plane.rs index a387e1e..ba9e12d 100644 --- a/harmony/src/modules/okd/bootstrap_03_control_plane.rs +++ b/harmony/src/modules/okd/bootstrap_03_control_plane.rs @@ -1,11 +1,3 @@ -use std::{fmt::Write, path::PathBuf}; - -use async_trait::async_trait; -use derive_new::new; -use harmony_types::id::Id; -use log::{debug, info}; -use serde::Serialize; - use crate::{ data::Version, hardware::PhysicalHost, @@ -19,6 +11,12 @@ use crate::{ score::Score, topology::{HAClusterTopology, HostBinding}, }; +use async_trait::async_trait; +use derive_new::new; +use harmony_types::id::Id; +use log::{debug, info}; +use serde::Serialize; + // ------------------------------------------------------------------------------------------------- // Step 03: Control Plane // - Render per-MAC PXE & ignition for cp0/cp1/cp2. @@ -269,8 +267,7 @@ impl Interpret for OKDSetup03ControlPlaneInterpret { // the `wait-for bootstrap-complete` command. info!("[ControlPlane] Provisioning initiated. Monitor the cluster convergence manually."); - Ok(Outcome::new( - InterpretStatus::SUCCESS, + Ok(Outcome::success( "Control plane provisioning has been successfully initiated.".into(), )) } diff --git a/harmony/src/modules/okd/bootstrap_04_workers.rs b/harmony/src/modules/okd/bootstrap_04_workers.rs index d5ed87c..461cab9 100644 --- a/harmony/src/modules/okd/bootstrap_04_workers.rs +++ b/harmony/src/modules/okd/bootstrap_04_workers.rs @@ -1,33 +1,17 @@ -use std::{fmt::Write, path::PathBuf}; - use async_trait::async_trait; use derive_new::new; -use harmony_secret::SecretManager; use harmony_types::id::Id; -use log::{debug, error, info, warn}; -use serde::{Deserialize, Serialize}; -use tokio::{fs::File, io::AsyncWriteExt, process::Command}; +use log::info; +use serde::Serialize; use crate::{ - config::secret::{RedhatSecret, SshKeyPair}, - data::{FileContent, FilePath, Version}, - hardware::PhysicalHost, - infra::inventory::InventoryRepositoryFactory, - instrumentation::{HarmonyEvent, instrument}, + data::Version, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, - inventory::{HostRole, Inventory}, - modules::{ - dhcp::DhcpHostBindingScore, - http::{IPxeMacBootFileScore, StaticFilesHttpScore}, - inventory::LaunchDiscoverInventoryAgentScore, - okd::{ - bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, - templates::{BootstrapIpxeTpl, InstallConfigYaml}, - }, - }, + inventory::Inventory, score::Score, - topology::{HAClusterTopology, HostBinding}, + topology::HAClusterTopology, }; + // ------------------------------------------------------------------------------------------------- // Step 04: Workers // - Render per-MAC PXE & ignition for workers; join nodes. @@ -94,9 +78,6 @@ impl Interpret for OKDSetup04WorkersInterpret { _topology: &HAClusterTopology, ) -> Result { self.render_and_reboot().await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, - "Workers provisioned".into(), - )) + Ok(Outcome::success("Workers provisioned".into())) } } diff --git a/harmony/src/modules/okd/bootstrap_05_sanity_check.rs b/harmony/src/modules/okd/bootstrap_05_sanity_check.rs index f1a4c2a..23a24b5 100644 --- a/harmony/src/modules/okd/bootstrap_05_sanity_check.rs +++ b/harmony/src/modules/okd/bootstrap_05_sanity_check.rs @@ -1,33 +1,16 @@ -use std::{fmt::Write, path::PathBuf}; - +use crate::{ + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::HAClusterTopology, +}; use async_trait::async_trait; use derive_new::new; -use harmony_secret::SecretManager; use harmony_types::id::Id; -use log::{debug, error, info, warn}; -use serde::{Deserialize, Serialize}; -use tokio::{fs::File, io::AsyncWriteExt, process::Command}; +use log::info; +use serde::Serialize; -use crate::{ - config::secret::{RedhatSecret, SshKeyPair}, - data::{FileContent, FilePath, Version}, - hardware::PhysicalHost, - infra::inventory::InventoryRepositoryFactory, - instrumentation::{HarmonyEvent, instrument}, - interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, - inventory::{HostRole, Inventory}, - modules::{ - dhcp::DhcpHostBindingScore, - http::{IPxeMacBootFileScore, StaticFilesHttpScore}, - inventory::LaunchDiscoverInventoryAgentScore, - okd::{ - bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, - templates::{BootstrapIpxeTpl, InstallConfigYaml}, - }, - }, - score::Score, - topology::{HAClusterTopology, HostBinding}, -}; // ------------------------------------------------------------------------------------------------- // Step 05: Sanity Check // - Validate API reachability, ClusterOperators, ingress, and SDN status. @@ -93,9 +76,6 @@ impl Interpret for OKDSetup05SanityCheckInterpret { _topology: &HAClusterTopology, ) -> Result { self.run_checks().await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, - "Sanity checks passed".into(), - )) + Ok(Outcome::success("Sanity checks passed".into())) } } diff --git a/harmony/src/modules/okd/bootstrap_06_installation_report.rs b/harmony/src/modules/okd/bootstrap_06_installation_report.rs index 2713bd2..07d379c 100644 --- a/harmony/src/modules/okd/bootstrap_06_installation_report.rs +++ b/harmony/src/modules/okd/bootstrap_06_installation_report.rs @@ -1,32 +1,15 @@ -// ------------------------------------------------------------------------------------------------- use async_trait::async_trait; use derive_new::new; -use harmony_secret::SecretManager; use harmony_types::id::Id; -use log::{debug, error, info, warn}; -use serde::{Deserialize, Serialize}; -use std::{fmt::Write, path::PathBuf}; -use tokio::{fs::File, io::AsyncWriteExt, process::Command}; +use log::info; +use serde::Serialize; use crate::{ - config::secret::{RedhatSecret, SshKeyPair}, - data::{FileContent, FilePath, Version}, - hardware::PhysicalHost, - infra::inventory::InventoryRepositoryFactory, - instrumentation::{HarmonyEvent, instrument}, + data::Version, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, - inventory::{HostRole, Inventory}, - modules::{ - dhcp::DhcpHostBindingScore, - http::{IPxeMacBootFileScore, StaticFilesHttpScore}, - inventory::LaunchDiscoverInventoryAgentScore, - okd::{ - bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, - templates::{BootstrapIpxeTpl, InstallConfigYaml}, - }, - }, + inventory::Inventory, score::Score, - topology::{HAClusterTopology, HostBinding}, + topology::HAClusterTopology, }; // Step 06: Installation Report @@ -93,9 +76,6 @@ impl Interpret for OKDSetup06InstallationReportInterpret { _topology: &HAClusterTopology, ) -> Result { self.generate().await?; - Ok(Outcome::new( - InterpretStatus::SUCCESS, - "Installation report generated".into(), - )) + Ok(Outcome::success("Installation report generated".into())) } } diff --git a/harmony_cli/src/cli_reporter.rs b/harmony_cli/src/cli_reporter.rs new file mode 100644 index 0000000..da43a8a --- /dev/null +++ b/harmony_cli/src/cli_reporter.rs @@ -0,0 +1,32 @@ +use std::sync::Mutex; + +use harmony::instrumentation::{self, HarmonyEvent}; +use log::info; + +pub fn init() { + let details: Mutex> = Mutex::new(vec![]); + + instrumentation::subscribe("Harmony CLI Reporter", { + move |event| { + let mut details = details.lock().unwrap(); + + if let HarmonyEvent::InterpretExecutionFinished { + execution_id: _, + topology: _, + interpret: _, + score: _, + outcome: Ok(outcome), + } = event + { + if outcome.status == harmony::interpret::InterpretStatus::SUCCESS { + details.extend(outcome.details.clone()); + } + } else if let HarmonyEvent::HarmonyFinished = event { + info!("Here's a summary of what happened:"); + for detail in details.iter() { + println!("{detail}"); + } + } + } + }); +} diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 0bfb1e7..4a0dbe7 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -8,6 +8,7 @@ use inquire::Confirm; use log::debug; pub mod cli_logger; // FIXME: Don't make me pub +mod cli_reporter; pub mod progress; pub mod theme; @@ -116,6 +117,7 @@ pub async fn run_cli( args: Args, ) -> Result<(), Box> { cli_logger::init(); + cli_reporter::init(); let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); maestro.register_all(scores); diff --git a/harmony_composer/src/harmony_composer_logger.rs b/harmony_composer/src/harmony_composer_logger.rs index 040a167..6351751 100644 --- a/harmony_composer/src/harmony_composer_logger.rs +++ b/harmony_composer/src/harmony_composer_logger.rs @@ -21,7 +21,6 @@ pub fn handle_events() { instrumentation::subscribe("Harmony Composer Logger", { move |event| match event { - HarmonyComposerEvent::HarmonyComposerStarted => {} HarmonyComposerEvent::ProjectInitializationStarted => { progress_tracker.add_section( SETUP_SECTION, diff --git a/harmony_composer/src/instrumentation.rs b/harmony_composer/src/instrumentation.rs index b9164b7..509d39c 100644 --- a/harmony_composer/src/instrumentation.rs +++ b/harmony_composer/src/instrumentation.rs @@ -5,7 +5,6 @@ use crate::{HarmonyProfile, HarmonyTarget}; #[derive(Debug, Clone)] pub enum HarmonyComposerEvent { - HarmonyComposerStarted, ProjectInitializationStarted, ProjectInitialized, ProjectCompilationStarted { -- 2.39.5 From f3639c604c9962ea81828deb6aa30562dc245d2c Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Tue, 9 Sep 2025 20:12:24 -0400 Subject: [PATCH 2/5] report Ntfy endpoint --- examples/try_rust_webapp/src/main.rs | 2 +- harmony/src/domain/interpret/mod.rs | 4 ++-- .../application_monitoring_score.rs | 4 +++- .../rhobs_application_monitoring_score.rs | 4 +++- harmony/src/modules/monitoring/ntfy/ntfy.rs | 8 +++++++- harmony_cli/src/cli_reporter.rs | 12 ++++++++---- harmony_cli/src/theme.rs | 1 + 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/examples/try_rust_webapp/src/main.rs b/examples/try_rust_webapp/src/main.rs index 26a1958..f51a9fa 100644 --- a/examples/try_rust_webapp/src/main.rs +++ b/examples/try_rust_webapp/src/main.rs @@ -18,7 +18,7 @@ async fn main() { name: "harmony-example-tryrust".to_string(), project_root: PathBuf::from("./tryrust.org"), framework: Some(RustWebFramework::Leptos), - service_port: 8080, + service_port: 3000, }); let discord_receiver = DiscordWebhook { diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 17afa86..d555d9e 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -88,10 +88,10 @@ pub struct Outcome { } impl Outcome { - pub fn noop() -> Self { + pub fn noop(message: String) -> Self { Self { status: InterpretStatus::NOOP, - message: String::new(), + message, details: vec![], } } diff --git a/harmony/src/modules/monitoring/application_monitoring/application_monitoring_score.rs b/harmony/src/modules/monitoring/application_monitoring/application_monitoring_score.rs index f4707a8..8246d15 100644 --- a/harmony/src/modules/monitoring/application_monitoring/application_monitoring_score.rs +++ b/harmony/src/modules/monitoring/application_monitoring/application_monitoring_score.rs @@ -68,7 +68,9 @@ impl> Interpret PreparationOutcome::Success { details: _ } => { Ok(Outcome::success("Prometheus installed".into())) } - PreparationOutcome::Noop => Ok(Outcome::noop()), + PreparationOutcome::Noop => { + Ok(Outcome::noop("Prometheus installation skipped".into())) + } }, Err(err) => Err(InterpretError::from(err)), } diff --git a/harmony/src/modules/monitoring/application_monitoring/rhobs_application_monitoring_score.rs b/harmony/src/modules/monitoring/application_monitoring/rhobs_application_monitoring_score.rs index 17e42c3..5f5127f 100644 --- a/harmony/src/modules/monitoring/application_monitoring/rhobs_application_monitoring_score.rs +++ b/harmony/src/modules/monitoring/application_monitoring/rhobs_application_monitoring_score.rs @@ -70,7 +70,9 @@ impl> Interpret PreparationOutcome::Success { details: _ } => { Ok(Outcome::success("Prometheus installed".into())) } - PreparationOutcome::Noop => Ok(Outcome::noop()), + PreparationOutcome::Noop => { + Ok(Outcome::noop("Prometheus installation skipped".into())) + } }, Err(err) => Err(InterpretError::from(err)), } diff --git a/harmony/src/modules/monitoring/ntfy/ntfy.rs b/harmony/src/modules/monitoring/ntfy/ntfy.rs index 87ed580..4ed342b 100644 --- a/harmony/src/modules/monitoring/ntfy/ntfy.rs +++ b/harmony/src/modules/monitoring/ntfy/ntfy.rs @@ -113,7 +113,13 @@ impl Interpret f .await?; info!("user added"); - Ok(Outcome::success("Ntfy installed".to_string())) + Ok(Outcome::success_with_details( + "Ntfy installed".to_string(), + vec![format!( + "Ntfy ({}): http://{}", + self.score.namespace, self.score.host + )], + )) } fn get_name(&self) -> InterpretName { diff --git a/harmony_cli/src/cli_reporter.rs b/harmony_cli/src/cli_reporter.rs index da43a8a..83570b6 100644 --- a/harmony_cli/src/cli_reporter.rs +++ b/harmony_cli/src/cli_reporter.rs @@ -1,7 +1,8 @@ use std::sync::Mutex; use harmony::instrumentation::{self, HarmonyEvent}; -use log::info; + +use crate::theme; pub fn init() { let details: Mutex> = Mutex::new(vec![]); @@ -21,11 +22,14 @@ pub fn init() { if outcome.status == harmony::interpret::InterpretStatus::SUCCESS { details.extend(outcome.details.clone()); } - } else if let HarmonyEvent::HarmonyFinished = event { - info!("Here's a summary of what happened:"); + } else if let HarmonyEvent::HarmonyFinished = event + && !details.is_empty() + { + println!("\n{} All done! What's next for you:", theme::EMOJI_SUMMARY); for detail in details.iter() { - println!("{detail}"); + println!("- {detail}"); } + println!(); } } }); diff --git a/harmony_cli/src/theme.rs b/harmony_cli/src/theme.rs index 66eee45..f9368f5 100644 --- a/harmony_cli/src/theme.rs +++ b/harmony_cli/src/theme.rs @@ -9,6 +9,7 @@ pub static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", ""); pub static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", ""); pub static EMOJI_TOPOLOGY: Emoji<'_, '_> = Emoji("📦", ""); pub static EMOJI_SCORE: Emoji<'_, '_> = Emoji("🎶", ""); +pub static EMOJI_SUMMARY: Emoji<'_, '_> = Emoji("🚀", ""); lazy_static! { pub static ref SECTION_STYLE: ProgressStyle = ProgressStyle::default_spinner() -- 2.39.5 From 7bc083701ee015e452e698ed9aaa1cfb9e0ea104 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Tue, 9 Sep 2025 22:18:00 -0400 Subject: [PATCH 3/5] report application deploy URL --- harmony/src/modules/application/feature.rs | 67 ++++++++++++++++++- .../features/continuous_delivery.rs | 13 +++- .../modules/application/features/endpoint.rs | 7 +- .../application/features/monitoring.rs | 11 ++- .../application/features/rhob_monitoring.rs | 11 ++- harmony/src/modules/application/mod.rs | 19 ++++-- harmony_cli/src/cli_logger.rs | 4 +- harmony_cli/src/cli_reporter.rs | 58 ++++++++++------ 8 files changed, 149 insertions(+), 41 deletions(-) diff --git a/harmony/src/modules/application/feature.rs b/harmony/src/modules/application/feature.rs index be4482f..9e1b1ae 100644 --- a/harmony/src/modules/application/feature.rs +++ b/harmony/src/modules/application/feature.rs @@ -1,7 +1,10 @@ +use std::error::Error; + use async_trait::async_trait; +use derive_new::new; use serde::Serialize; -use crate::topology::Topology; +use crate::{executors::ExecutorError, topology::Topology}; /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability, /// ContinuousIntegration, ContinuousDelivery @@ -9,7 +12,10 @@ use crate::topology::Topology; pub trait ApplicationFeature: std::fmt::Debug + Send + Sync + ApplicationFeatureClone { - async fn ensure_installed(&self, topology: &T) -> Result<(), String>; + async fn ensure_installed( + &self, + topology: &T, + ) -> Result; fn name(&self) -> String; } @@ -40,3 +46,60 @@ impl Clone for Box> { self.clone_box() } } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InstallationOutcome { + Success { details: Vec }, + Noop, +} + +impl InstallationOutcome { + pub fn success() -> Self { + Self::Success { details: vec![] } + } + + pub fn success_with_details(details: Vec) -> Self { + Self::Success { details } + } + + pub fn noop() -> Self { + Self::Noop + } +} + +#[derive(Debug, Clone, new)] +pub struct InstallationError { + msg: String, +} + +impl std::fmt::Display for InstallationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.msg) + } +} + +impl Error for InstallationError {} + +impl From for InstallationError { + fn from(value: ExecutorError) -> Self { + Self { + msg: format!("InstallationError : {value}"), + } + } +} + +impl From for InstallationError { + fn from(value: kube::Error) -> Self { + Self { + msg: format!("InstallationError : {value}"), + } + } +} + +impl From for InstallationError { + fn from(value: String) -> Self { + Self { + msg: format!("PreparationError : {value}"), + } + } +} diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs index 63e34a6..703da4a 100644 --- a/harmony/src/modules/application/features/continuous_delivery.rs +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -10,7 +10,7 @@ use crate::{ data::Version, inventory::Inventory, modules::application::{ - ApplicationFeature, HelmPackage, OCICompliant, + ApplicationFeature, HelmPackage, InstallationError, InstallationOutcome, OCICompliant, features::{ArgoApplication, ArgoHelmScore}, }, score::Score, @@ -141,7 +141,10 @@ impl< T: Topology + HelmCommand + MultiTargetTopology + K8sclient + Ingress + 'static, > ApplicationFeature for ContinuousDelivery { - async fn ensure_installed(&self, topology: &T) -> Result<(), String> { + async fn ensure_installed( + &self, + topology: &T, + ) -> Result { let image = self.application.image_name(); let domain = topology .get_domain(&self.application.name()) @@ -205,7 +208,11 @@ impl< .unwrap(); } }; - Ok(()) + + Ok(InstallationOutcome::success_with_details(vec![format!( + "{}: {domain}", + self.application.name() + )])) } fn name(&self) -> String { "ContinuousDelivery".to_string() diff --git a/harmony/src/modules/application/features/endpoint.rs b/harmony/src/modules/application/features/endpoint.rs index 042f0dd..d2b23db 100644 --- a/harmony/src/modules/application/features/endpoint.rs +++ b/harmony/src/modules/application/features/endpoint.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use log::info; use crate::{ - modules::application::ApplicationFeature, + modules::application::{ApplicationFeature, InstallationError, InstallationOutcome}, topology::{K8sclient, Topology}, }; @@ -29,7 +29,10 @@ impl Default for PublicEndpoint { /// For now we only suport K8s ingress, but we will support more stuff at some point #[async_trait] impl ApplicationFeature for PublicEndpoint { - async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { + async fn ensure_installed( + &self, + _topology: &T, + ) -> Result { info!( "Making sure public endpoint is installed for port {}", self.application_port diff --git a/harmony/src/modules/application/features/monitoring.rs b/harmony/src/modules/application/features/monitoring.rs index b8531fe..1a60d00 100644 --- a/harmony/src/modules/application/features/monitoring.rs +++ b/harmony/src/modules/application/features/monitoring.rs @@ -1,4 +1,6 @@ -use crate::modules::application::{Application, ApplicationFeature}; +use crate::modules::application::{ + Application, ApplicationFeature, InstallationError, InstallationOutcome, +}; use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore; use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus; use crate::topology::MultiTargetTopology; @@ -43,7 +45,10 @@ impl< + std::fmt::Debug, > ApplicationFeature for Monitoring { - async fn ensure_installed(&self, topology: &T) -> Result<(), String> { + async fn ensure_installed( + &self, + topology: &T, + ) -> Result { info!("Ensuring monitoring is available for application"); let namespace = topology .get_tenant_config() @@ -103,7 +108,7 @@ impl< .await .map_err(|e| e.to_string())?; - Ok(()) + Ok(InstallationOutcome::success()) } fn name(&self) -> String { diff --git a/harmony/src/modules/application/features/rhob_monitoring.rs b/harmony/src/modules/application/features/rhob_monitoring.rs index e6f51a4..9075751 100644 --- a/harmony/src/modules/application/features/rhob_monitoring.rs +++ b/harmony/src/modules/application/features/rhob_monitoring.rs @@ -1,6 +1,8 @@ use std::sync::Arc; -use crate::modules::application::{Application, ApplicationFeature}; +use crate::modules::application::{ + Application, ApplicationFeature, InstallationError, InstallationOutcome, +}; use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore; use crate::modules::monitoring::application_monitoring::rhobs_application_monitoring_score::ApplicationRHOBMonitoringScore; @@ -43,7 +45,10 @@ impl< + PrometheusApplicationMonitoring, > ApplicationFeature for RHOBMonitoring { - async fn ensure_installed(&self, topology: &T) -> Result<(), String> { + async fn ensure_installed( + &self, + topology: &T, + ) -> Result { info!("Ensuring monitoring is available for application"); let namespace = topology .get_tenant_config() @@ -106,7 +111,7 @@ impl< .interpret(&Inventory::empty(), topology) .await .map_err(|e| e.to_string())?; - Ok(()) + Ok(InstallationOutcome::success()) } fn name(&self) -> String { "Monitoring".to_string() diff --git a/harmony/src/modules/application/mod.rs b/harmony/src/modules/application/mod.rs index 8e60984..b7bb973 100644 --- a/harmony/src/modules/application/mod.rs +++ b/harmony/src/modules/application/mod.rs @@ -24,8 +24,8 @@ use harmony_types::id::Id; #[derive(Clone, Debug)] pub enum ApplicationFeatureStatus { Installing, - Installed, - Failed { details: String }, + Installed { details: Vec }, + Failed { message: String }, } pub trait Application: std::fmt::Debug + Send + Sync { @@ -65,27 +65,32 @@ impl Interpret for Application .unwrap(); let _ = match feature.ensure_installed(topology).await { - Ok(()) => { + Ok(outcome) => { instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { topology: topology.name().into(), application: self.application.name(), feature: feature.name(), - status: ApplicationFeatureStatus::Installed, + status: ApplicationFeatureStatus::Installed { + details: match outcome { + InstallationOutcome::Success { details } => details, + InstallationOutcome::Noop => vec![], + }, + }, }) .unwrap(); } - Err(msg) => { + Err(error) => { instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { topology: topology.name().into(), application: self.application.name(), feature: feature.name(), status: ApplicationFeatureStatus::Failed { - details: msg.clone(), + message: error.to_string(), }, }) .unwrap(); return Err(InterpretError::new(format!( - "Application Interpret failed to install feature : {msg}" + "Application Interpret failed to install feature : {error}" ))); } }; diff --git a/harmony_cli/src/cli_logger.rs b/harmony_cli/src/cli_logger.rs index be61c2a..2cb2a93 100644 --- a/harmony_cli/src/cli_logger.rs +++ b/harmony_cli/src/cli_logger.rs @@ -178,10 +178,10 @@ fn handle_events() { ApplicationFeatureStatus::Installing => { info!("Installing feature '{feature}' for '{application}'..."); } - ApplicationFeatureStatus::Installed => { + ApplicationFeatureStatus::Installed { details: _ } => { info!(status = "finished"; "Feature '{feature}' installed"); } - ApplicationFeatureStatus::Failed { details } => { + ApplicationFeatureStatus::Failed { message: details } => { error!(status = "failed"; "Feature '{feature}' installation failed: {details}"); } }, diff --git a/harmony_cli/src/cli_reporter.rs b/harmony_cli/src/cli_reporter.rs index 83570b6..f6095cc 100644 --- a/harmony_cli/src/cli_reporter.rs +++ b/harmony_cli/src/cli_reporter.rs @@ -1,6 +1,9 @@ use std::sync::Mutex; -use harmony::instrumentation::{self, HarmonyEvent}; +use harmony::{ + instrumentation::{self, HarmonyEvent}, + modules::application::ApplicationFeatureStatus, +}; use crate::theme; @@ -11,26 +14,43 @@ pub fn init() { move |event| { let mut details = details.lock().unwrap(); - if let HarmonyEvent::InterpretExecutionFinished { - execution_id: _, - topology: _, - interpret: _, - score: _, - outcome: Ok(outcome), - } = event - { - if outcome.status == harmony::interpret::InterpretStatus::SUCCESS { - details.extend(outcome.details.clone()); + match event { + HarmonyEvent::InterpretExecutionFinished { + execution_id: _, + topology: _, + interpret: _, + score: _, + outcome: Ok(outcome), + } => { + if outcome.status == harmony::interpret::InterpretStatus::SUCCESS { + details.extend(outcome.details.clone()); + } } - } else if let HarmonyEvent::HarmonyFinished = event - && !details.is_empty() - { - println!("\n{} All done! What's next for you:", theme::EMOJI_SUMMARY); - for detail in details.iter() { - println!("- {detail}"); + HarmonyEvent::ApplicationFeatureStateChanged { + topology: _, + application: _, + feature: _, + status: + ApplicationFeatureStatus::Installed { + details: feature_details, + }, + } => { + details.extend(feature_details.clone()); } - println!(); - } + HarmonyEvent::HarmonyFinished => { + if !details.is_empty() { + println!( + "\n{} All done! Here's what's next for you:", + theme::EMOJI_SUMMARY + ); + for detail in details.iter() { + println!("- {detail}"); + } + println!(); + } + } + _ => {} + }; } }); } -- 2.39.5 From 84247788712822628ff0e782514e526100e4f726 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Tue, 9 Sep 2025 22:24:36 -0400 Subject: [PATCH 4/5] add http --- examples/try_rust_webapp/src/main.rs | 2 +- harmony/src/modules/application/features/continuous_delivery.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/try_rust_webapp/src/main.rs b/examples/try_rust_webapp/src/main.rs index f51a9fa..26a1958 100644 --- a/examples/try_rust_webapp/src/main.rs +++ b/examples/try_rust_webapp/src/main.rs @@ -18,7 +18,7 @@ async fn main() { name: "harmony-example-tryrust".to_string(), project_root: PathBuf::from("./tryrust.org"), framework: Some(RustWebFramework::Leptos), - service_port: 3000, + service_port: 8080, }); let discord_receiver = DiscordWebhook { diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs index 703da4a..beb422d 100644 --- a/harmony/src/modules/application/features/continuous_delivery.rs +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -210,7 +210,7 @@ impl< }; Ok(InstallationOutcome::success_with_details(vec![format!( - "{}: {domain}", + "{}: http://{domain}", self.application.name() )])) } -- 2.39.5 From 7514ebfb5c01c95345231e194570e42e74d18aaf Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Tue, 9 Sep 2025 22:50:26 -0400 Subject: [PATCH 5/5] fix format --- harmony/src/domain/topology/k8s_anywhere.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index bb7dc6a..931433a 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -372,7 +372,9 @@ impl K8sAnywhereTopology { if let Some(Some(k8s_state)) = self.k8s_state.get() { match k8s_state.source { K8sSource::LocalK3d => { - warn!("Installing observability operator is not supported on LocalK3d source"); + warn!( + "Installing observability operator is not supported on LocalK3d source" + ); return Ok(PreparationOutcome::Noop); debug!("installing cluster observability operator"); todo!(); -- 2.39.5