feat: Report execution outcome #151
| @ -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,13 +84,15 @@ pub trait Interpret<T>: std::fmt::Debug + Send { | ||||
| pub struct Outcome { | ||||
|     pub status: InterpretStatus, | ||||
|     pub message: String, | ||||
|     pub details: Vec<String>, | ||||
| } | ||||
| 
 | ||||
| impl Outcome { | ||||
|     pub fn noop() -> Self { | ||||
|     pub fn noop(message: String) -> Self { | ||||
|         Self { | ||||
|             status: InterpretStatus::NOOP, | ||||
|             message: String::new(), | ||||
|             message, | ||||
|             details: vec![], | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -96,6 +100,23 @@ impl Outcome { | ||||
|         Self { | ||||
|             status: InterpretStatus::SUCCESS, | ||||
|             message, | ||||
|             details: vec![], | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn success_with_details(message: String, details: Vec<String>) -> Self { | ||||
|         Self { | ||||
|             status: InterpretStatus::SUCCESS, | ||||
|             message, | ||||
|             details, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn running(message: String) -> Self { | ||||
|         Self { | ||||
|             status: InterpretStatus::RUNNING, | ||||
|             message, | ||||
|             details: vec![], | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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!(); | ||||
|  | ||||
| @ -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<T: Topology>: | ||||
|     std::fmt::Debug + Send + Sync + ApplicationFeatureClone<T> | ||||
| { | ||||
|     async fn ensure_installed(&self, topology: &T) -> Result<(), String>; | ||||
|     async fn ensure_installed( | ||||
|         &self, | ||||
|         topology: &T, | ||||
|     ) -> Result<InstallationOutcome, InstallationError>; | ||||
|     fn name(&self) -> String; | ||||
| } | ||||
| 
 | ||||
| @ -40,3 +46,60 @@ impl<T: Topology> Clone for Box<dyn ApplicationFeature<T>> { | ||||
|         self.clone_box() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||||
| pub enum InstallationOutcome { | ||||
|     Success { details: Vec<String> }, | ||||
|     Noop, | ||||
| } | ||||
| 
 | ||||
| impl InstallationOutcome { | ||||
|     pub fn success() -> Self { | ||||
|         Self::Success { details: vec![] } | ||||
|     } | ||||
| 
 | ||||
|     pub fn success_with_details(details: Vec<String>) -> 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<ExecutorError> for InstallationError { | ||||
|     fn from(value: ExecutorError) -> Self { | ||||
|         Self { | ||||
|             msg: format!("InstallationError : {value}"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<kube::Error> for InstallationError { | ||||
|     fn from(value: kube::Error) -> Self { | ||||
|         Self { | ||||
|             msg: format!("InstallationError : {value}"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<String> for InstallationError { | ||||
|     fn from(value: String) -> Self { | ||||
|         Self { | ||||
|             msg: format!("PreparationError : {value}"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<T> for ContinuousDelivery<A> | ||||
| { | ||||
|     async fn ensure_installed(&self, topology: &T) -> Result<(), String> { | ||||
|     async fn ensure_installed( | ||||
|         &self, | ||||
|         topology: &T, | ||||
|     ) -> Result<InstallationOutcome, InstallationError> { | ||||
|         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!( | ||||
|             "{}: http://{domain}", | ||||
|             self.application.name() | ||||
|         )])) | ||||
|     } | ||||
|     fn name(&self) -> String { | ||||
|         "ContinuousDelivery".to_string() | ||||
|  | ||||
| @ -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<T: Topology + K8sclient + 'static> ApplicationFeature<T> for PublicEndpoint { | ||||
|     async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { | ||||
|     async fn ensure_installed( | ||||
|         &self, | ||||
|         _topology: &T, | ||||
|     ) -> Result<InstallationOutcome, InstallationError> { | ||||
|         info!( | ||||
|             "Making sure public endpoint is installed for port {}", | ||||
|             self.application_port | ||||
|  | ||||
| @ -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<T> for Monitoring | ||||
| { | ||||
|     async fn ensure_installed(&self, topology: &T) -> Result<(), String> { | ||||
|     async fn ensure_installed( | ||||
|         &self, | ||||
|         topology: &T, | ||||
|     ) -> Result<InstallationOutcome, InstallationError> { | ||||
|         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 { | ||||
|  | ||||
| @ -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<RHOBObservability>, | ||||
| > ApplicationFeature<T> for RHOBMonitoring | ||||
| { | ||||
|     async fn ensure_installed(&self, topology: &T) -> Result<(), String> { | ||||
|     async fn ensure_installed( | ||||
|         &self, | ||||
|         topology: &T, | ||||
|     ) -> Result<InstallationOutcome, InstallationError> { | ||||
|         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() | ||||
|  | ||||
| @ -24,8 +24,8 @@ use harmony_types::id::Id; | ||||
| #[derive(Clone, Debug)] | ||||
| pub enum ApplicationFeatureStatus { | ||||
|     Installing, | ||||
|     Installed, | ||||
|     Failed { details: String }, | ||||
|     Installed { details: Vec<String> }, | ||||
|     Failed { message: String }, | ||||
| } | ||||
| 
 | ||||
| pub trait Application: std::fmt::Debug + Send + Sync { | ||||
| @ -65,27 +65,32 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> 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}" | ||||
|                     ))); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
| @ -69,17 +69,14 @@ impl DhcpInterpret { | ||||
| 
 | ||||
|         dhcp_server.set_pxe_options(pxe_options).await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             format!( | ||||
|         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<T: Topology + DhcpServer> Interpret<T> 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<T: DhcpServer> Interpret<T> for DhcpHostBindingInterpret { | ||||
| 
 | ||||
|         topology.commit_config().await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             format!( | ||||
|         Ok(Outcome::success(format!( | ||||
|             "Dhcp Host Binding Interpret execution successful on {} hosts", | ||||
|             self.score.host_binding.len() | ||||
|             ), | ||||
|         )) | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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!( | ||||
|         Ok(Outcome::success(format!( | ||||
|             "DnsInterpret registered {} hosts successfully", | ||||
|             entries.len() | ||||
|             ), | ||||
|         )) | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -111,8 +107,7 @@ impl<T: Topology + DnsServer> Interpret<T> for DnsInterpret { | ||||
| 
 | ||||
|         topology.commit_config().await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|         Ok(Outcome::success( | ||||
|             "Dns Interpret execution successful".to_string(), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
| @ -197,13 +197,10 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret { | ||||
|                     self.score.release_name, ns | ||||
|                 ); | ||||
| 
 | ||||
|                 return Ok(Outcome::new( | ||||
|                     InterpretStatus::SUCCESS, | ||||
|                     format!( | ||||
|                 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<T: Topology + HelmCommand> Interpret<T> 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 | ||||
|  | ||||
| @ -133,10 +133,9 @@ impl<T: Topology> Interpret<T> 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 { | ||||
|  | ||||
| @ -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<T: Topology + K8sclient> Score<T> 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<T: Topology + K8sclient> Score<T> 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<T: Topology + K8sclient> Score<T> for K8sIngressScore { | ||||
|         format!("{} K8sIngressScore", self.name) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(std::fmt::Debug)] | ||||
| struct K8sIngressInterpret { | ||||
|     ingress: Ingress, | ||||
|     service: String, | ||||
|     namespace: Option<String>, | ||||
|     host: fqdn::FQDN, | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology + K8sclient> Interpret<T> for K8sIngressInterpret { | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         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<Id> { | ||||
|         vec![] | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -68,7 +68,9 @@ impl<T: Topology + PrometheusApplicationMonitoring<CRDPrometheus>> Interpret<T> | ||||
|                 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)), | ||||
|         } | ||||
|  | ||||
| @ -70,7 +70,9 @@ impl<T: Topology + PrometheusApplicationMonitoring<RHOBObservability>> 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)), | ||||
|         } | ||||
|  | ||||
| @ -113,7 +113,13 @@ impl<T: Topology + HelmCommand + K8sclient + MultiTargetTopology> Interpret<T> 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 { | ||||
|  | ||||
| @ -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!( | ||||
|         Ok(Outcome::success(format!( | ||||
|             "Found and assigned bootstrap node: {}", | ||||
|             bootstrap_host.unwrap().summary() | ||||
|             ), | ||||
|         )) | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<HAClusterTopology> 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())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<HAClusterTopology> 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(), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
| @ -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<HAClusterTopology> for OKDSetup04WorkersInterpret { | ||||
|         _topology: &HAClusterTopology, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.render_and_reboot().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Workers provisioned".into(), | ||||
|         )) | ||||
|         Ok(Outcome::success("Workers provisioned".into())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<HAClusterTopology> for OKDSetup05SanityCheckInterpret { | ||||
|         _topology: &HAClusterTopology, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.run_checks().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Sanity checks passed".into(), | ||||
|         )) | ||||
|         Ok(Outcome::success("Sanity checks passed".into())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<HAClusterTopology> for OKDSetup06InstallationReportInterpret { | ||||
|         _topology: &HAClusterTopology, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.generate().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Installation report generated".into(), | ||||
|         )) | ||||
|         Ok(Outcome::success("Installation report generated".into())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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}"); | ||||
|                     } | ||||
|                 }, | ||||
|  | ||||
							
								
								
									
										56
									
								
								harmony_cli/src/cli_reporter.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								harmony_cli/src/cli_reporter.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| use std::sync::Mutex; | ||||
| 
 | ||||
| use harmony::{ | ||||
|     instrumentation::{self, HarmonyEvent}, | ||||
|     modules::application::ApplicationFeatureStatus, | ||||
| }; | ||||
| 
 | ||||
| use crate::theme; | ||||
| 
 | ||||
| pub fn init() { | ||||
|     let details: Mutex<Vec<String>> = Mutex::new(vec![]); | ||||
| 
 | ||||
|     instrumentation::subscribe("Harmony CLI Reporter", { | ||||
|         move |event| { | ||||
|             let mut details = details.lock().unwrap(); | ||||
| 
 | ||||
|             match event { | ||||
|                 HarmonyEvent::InterpretExecutionFinished { | ||||
|                     execution_id: _, | ||||
|                     topology: _, | ||||
|                     interpret: _, | ||||
|                     score: _, | ||||
|                     outcome: Ok(outcome), | ||||
|                 } => { | ||||
|                     if outcome.status == harmony::interpret::InterpretStatus::SUCCESS { | ||||
|                         details.extend(outcome.details.clone()); | ||||
|                     } | ||||
|                 } | ||||
|                 HarmonyEvent::ApplicationFeatureStateChanged { | ||||
|                     topology: _, | ||||
|                     application: _, | ||||
|                     feature: _, | ||||
|                     status: | ||||
|                         ApplicationFeatureStatus::Installed { | ||||
|                             details: feature_details, | ||||
|                         }, | ||||
|                 } => { | ||||
|                     details.extend(feature_details.clone()); | ||||
|                 } | ||||
|                 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!(); | ||||
|                     } | ||||
|                 } | ||||
|                 _ => {} | ||||
|             }; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| @ -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<T: Topology + Send + Sync + 'static>( | ||||
|     args: Args, | ||||
| ) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     cli_logger::init(); | ||||
|     cli_reporter::init(); | ||||
| 
 | ||||
|     let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); | ||||
|     maestro.register_all(scores); | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -5,7 +5,6 @@ use crate::{HarmonyProfile, HarmonyTarget}; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum HarmonyComposerEvent { | ||||
|     HarmonyComposerStarted, | ||||
|     ProjectInitializationStarted, | ||||
|     ProjectInitialized, | ||||
|     ProjectCompilationStarted { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user