report application deploy URL

This commit is contained in:
Ian Letourneau 2025-09-09 22:18:00 -04:00
parent f3639c604c
commit 7bc083701e
8 changed files with 149 additions and 41 deletions

View File

@ -1,7 +1,10 @@
use std::error::Error;
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new;
use serde::Serialize; use serde::Serialize;
use crate::topology::Topology; use crate::{executors::ExecutorError, topology::Topology};
/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability, /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
/// ContinuousIntegration, ContinuousDelivery /// ContinuousIntegration, ContinuousDelivery
@ -9,7 +12,10 @@ use crate::topology::Topology;
pub trait ApplicationFeature<T: Topology>: pub trait ApplicationFeature<T: Topology>:
std::fmt::Debug + Send + Sync + ApplicationFeatureClone<T> 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; fn name(&self) -> String;
} }
@ -40,3 +46,60 @@ impl<T: Topology> Clone for Box<dyn ApplicationFeature<T>> {
self.clone_box() 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}"),
}
}
}

View File

@ -10,7 +10,7 @@ use crate::{
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::application::{ modules::application::{
ApplicationFeature, HelmPackage, OCICompliant, ApplicationFeature, HelmPackage, InstallationError, InstallationOutcome, OCICompliant,
features::{ArgoApplication, ArgoHelmScore}, features::{ArgoApplication, ArgoHelmScore},
}, },
score::Score, score::Score,
@ -141,7 +141,10 @@ impl<
T: Topology + HelmCommand + MultiTargetTopology + K8sclient + Ingress + 'static, T: Topology + HelmCommand + MultiTargetTopology + K8sclient + Ingress + 'static,
> ApplicationFeature<T> for ContinuousDelivery<A> > 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 image = self.application.image_name();
let domain = topology let domain = topology
.get_domain(&self.application.name()) .get_domain(&self.application.name())
@ -205,7 +208,11 @@ impl<
.unwrap(); .unwrap();
} }
}; };
Ok(())
Ok(InstallationOutcome::success_with_details(vec![format!(
"{}: {domain}",
self.application.name()
)]))
} }
fn name(&self) -> String { fn name(&self) -> String {
"ContinuousDelivery".to_string() "ContinuousDelivery".to_string()

View File

@ -2,7 +2,7 @@ use async_trait::async_trait;
use log::info; use log::info;
use crate::{ use crate::{
modules::application::ApplicationFeature, modules::application::{ApplicationFeature, InstallationError, InstallationOutcome},
topology::{K8sclient, Topology}, 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 /// For now we only suport K8s ingress, but we will support more stuff at some point
#[async_trait] #[async_trait]
impl<T: Topology + K8sclient + 'static> ApplicationFeature<T> for PublicEndpoint { 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!( info!(
"Making sure public endpoint is installed for port {}", "Making sure public endpoint is installed for port {}",
self.application_port self.application_port

View File

@ -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::application_monitoring::application_monitoring_score::ApplicationMonitoringScore;
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus; use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus;
use crate::topology::MultiTargetTopology; use crate::topology::MultiTargetTopology;
@ -43,7 +45,10 @@ impl<
+ std::fmt::Debug, + std::fmt::Debug,
> ApplicationFeature<T> for Monitoring > 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"); info!("Ensuring monitoring is available for application");
let namespace = topology let namespace = topology
.get_tenant_config() .get_tenant_config()
@ -103,7 +108,7 @@ impl<
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(()) Ok(InstallationOutcome::success())
} }
fn name(&self) -> String { fn name(&self) -> String {

View File

@ -1,6 +1,8 @@
use std::sync::Arc; 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::application_monitoring_score::ApplicationMonitoringScore;
use crate::modules::monitoring::application_monitoring::rhobs_application_monitoring_score::ApplicationRHOBMonitoringScore; use crate::modules::monitoring::application_monitoring::rhobs_application_monitoring_score::ApplicationRHOBMonitoringScore;
@ -43,7 +45,10 @@ impl<
+ PrometheusApplicationMonitoring<RHOBObservability>, + PrometheusApplicationMonitoring<RHOBObservability>,
> ApplicationFeature<T> for RHOBMonitoring > 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"); info!("Ensuring monitoring is available for application");
let namespace = topology let namespace = topology
.get_tenant_config() .get_tenant_config()
@ -106,7 +111,7 @@ impl<
.interpret(&Inventory::empty(), topology) .interpret(&Inventory::empty(), topology)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(()) Ok(InstallationOutcome::success())
} }
fn name(&self) -> String { fn name(&self) -> String {
"Monitoring".to_string() "Monitoring".to_string()

View File

@ -24,8 +24,8 @@ use harmony_types::id::Id;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ApplicationFeatureStatus { pub enum ApplicationFeatureStatus {
Installing, Installing,
Installed, Installed { details: Vec<String> },
Failed { details: String }, Failed { message: String },
} }
pub trait Application: std::fmt::Debug + Send + Sync { 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(); .unwrap();
let _ = match feature.ensure_installed(topology).await { let _ = match feature.ensure_installed(topology).await {
Ok(()) => { Ok(outcome) => {
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
topology: topology.name().into(), topology: topology.name().into(),
application: self.application.name(), application: self.application.name(),
feature: feature.name(), feature: feature.name(),
status: ApplicationFeatureStatus::Installed, status: ApplicationFeatureStatus::Installed {
details: match outcome {
InstallationOutcome::Success { details } => details,
InstallationOutcome::Noop => vec![],
},
},
}) })
.unwrap(); .unwrap();
} }
Err(msg) => { Err(error) => {
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
topology: topology.name().into(), topology: topology.name().into(),
application: self.application.name(), application: self.application.name(),
feature: feature.name(), feature: feature.name(),
status: ApplicationFeatureStatus::Failed { status: ApplicationFeatureStatus::Failed {
details: msg.clone(), message: error.to_string(),
}, },
}) })
.unwrap(); .unwrap();
return Err(InterpretError::new(format!( return Err(InterpretError::new(format!(
"Application Interpret failed to install feature : {msg}" "Application Interpret failed to install feature : {error}"
))); )));
} }
}; };

View File

@ -178,10 +178,10 @@ fn handle_events() {
ApplicationFeatureStatus::Installing => { ApplicationFeatureStatus::Installing => {
info!("Installing feature '{feature}' for '{application}'..."); info!("Installing feature '{feature}' for '{application}'...");
} }
ApplicationFeatureStatus::Installed => { ApplicationFeatureStatus::Installed { details: _ } => {
info!(status = "finished"; "Feature '{feature}' installed"); info!(status = "finished"; "Feature '{feature}' installed");
} }
ApplicationFeatureStatus::Failed { details } => { ApplicationFeatureStatus::Failed { message: details } => {
error!(status = "failed"; "Feature '{feature}' installation failed: {details}"); error!(status = "failed"; "Feature '{feature}' installation failed: {details}");
} }
}, },

View File

@ -1,6 +1,9 @@
use std::sync::Mutex; use std::sync::Mutex;
use harmony::instrumentation::{self, HarmonyEvent}; use harmony::{
instrumentation::{self, HarmonyEvent},
modules::application::ApplicationFeatureStatus,
};
use crate::theme; use crate::theme;
@ -11,26 +14,43 @@ pub fn init() {
move |event| { move |event| {
let mut details = details.lock().unwrap(); let mut details = details.lock().unwrap();
if let HarmonyEvent::InterpretExecutionFinished { match event {
execution_id: _, HarmonyEvent::InterpretExecutionFinished {
topology: _, execution_id: _,
interpret: _, topology: _,
score: _, interpret: _,
outcome: Ok(outcome), score: _,
} = event outcome: Ok(outcome),
{ } => {
if outcome.status == harmony::interpret::InterpretStatus::SUCCESS { if outcome.status == harmony::interpret::InterpretStatus::SUCCESS {
details.extend(outcome.details.clone()); details.extend(outcome.details.clone());
}
} }
} else if let HarmonyEvent::HarmonyFinished = event HarmonyEvent::ApplicationFeatureStateChanged {
&& !details.is_empty() topology: _,
{ application: _,
println!("\n{} All done! What's next for you:", theme::EMOJI_SUMMARY); feature: _,
for detail in details.iter() { status:
println!("- {detail}"); 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!();
}
}
_ => {}
};
} }
}); });
} }