refactor(prometheus): modified crd prometheus to impl the installable trait

This commit is contained in:
Willem 2025-10-09 12:26:05 -04:00
parent 58b6268989
commit 1f3796f503
8 changed files with 219 additions and 112 deletions

View File

@ -3,7 +3,7 @@ use harmony::{
modules::{ modules::{
application::{ application::{
ApplicationScore, RustWebFramework, RustWebapp, ApplicationScore, RustWebFramework, RustWebapp,
features::{PackagingDeployment, rhob_monitoring::Monitoring}, features::{Monitoring, PackagingDeployment},
}, },
monitoring::alert_channel::discord_alert_channel::DiscordWebhook, monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
}, },

View File

@ -1,7 +1,7 @@
use std::{process::Command, sync::Arc}; use std::{collections::BTreeMap, process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use kube::api::GroupVersionKind; use kube::api::{GroupVersionKind, ObjectMeta};
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::Serialize; use serde::Serialize;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
@ -12,12 +12,20 @@ use crate::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
k3d::K3DInstallationScore, k3d::K3DInstallationScore,
k8s::ingress::{K8sIngressScore, PathType},
monitoring::{ monitoring::{
grafana::{grafana::Grafana, helm::helm_grafana::grafana_helm_chart_score}, grafana::{grafana::Grafana, helm::helm_grafana::grafana_helm_chart_score},
kube_prometheus::crd::{ kube_prometheus::crd::{
crd_alertmanager_config::CRDPrometheus, crd_alertmanager_config::CRDPrometheus,
crd_grafana::{
Grafana as GrafanaCRD, GrafanaDashboard, GrafanaDashboardSpec,
GrafanaDatasource, GrafanaDatasourceConfig, GrafanaDatasourceSpec, GrafanaSpec,
},
crd_prometheuses::LabelSelector,
grafana_default_dashboard::build_default_dashboard,
prometheus_operator::prometheus_operator_helm_chart_score, prometheus_operator::prometheus_operator_helm_chart_score,
rhob_alertmanager_config::RHOBObservability, service_monitor::ServiceMonitor, rhob_alertmanager_config::RHOBObservability,
service_monitor::ServiceMonitor,
}, },
}, },
prometheus::{ prometheus::{
@ -90,10 +98,11 @@ impl K8sclient for K8sAnywhereTopology {
#[async_trait] #[async_trait]
impl Grafana for K8sAnywhereTopology { impl Grafana for K8sAnywhereTopology {
async fn ensure_grafana_operator_ready( async fn ensure_grafana_operator(
&self, &self,
inventory: &Inventory, inventory: &Inventory,
) -> Result<PreparationOutcome, PreparationError> { ) -> Result<PreparationOutcome, PreparationError> {
debug!("ensure grafana operator");
let client = self.k8s_client().await.unwrap(); let client = self.k8s_client().await.unwrap();
let grafana_gvk = GroupVersionKind { let grafana_gvk = GroupVersionKind {
group: "grafana.integreatly.org".to_string(), group: "grafana.integreatly.org".to_string(),
@ -112,6 +121,7 @@ impl Grafana for K8sAnywhereTopology {
details: "Found grafana CRDs in cluster".to_string(), details: "Found grafana CRDs in cluster".to_string(),
}); });
} }
Err(_) => { Err(_) => {
return self return self
.install_grafana_operator(inventory, Some("grafana")) .install_grafana_operator(inventory, Some("grafana"))
@ -120,7 +130,41 @@ impl Grafana for K8sAnywhereTopology {
}; };
} }
async fn install_grafana(&self) -> Result<PreparationOutcome, PreparationError> { async fn install_grafana(&self) -> Result<PreparationOutcome, PreparationError> {
todo!() debug!("install grafana");
let ns = "grafana";
let mut label = BTreeMap::new();
label.insert("dashboards".to_string(), "grafana".to_string());
let label_selector = LabelSelector {
match_labels: label.clone(),
match_expressions: vec![],
};
let client = self.k8s_client().await?;
let datasource = self.build_grafana_datasource(ns, &label_selector);
client.apply(&datasource, Some(ns)).await?;
let dashboard = self.build_grafana_dashboard(ns, &label_selector);
client.apply(&dashboard, Some(ns)).await?;
let grafana = self.build_grafana(ns, &label);
client.apply(&grafana, Some(ns)).await?;
let grafana_ingress = self.build_grafana_ingress(ns).await;
grafana_ingress
.interpret(&Inventory::empty(), self)
.await
.map_err(|e| PreparationError::new(e.to_string()))?;
Ok(PreparationOutcome::Success {
details: "Installed grafana composants".to_string(),
})
} }
} }
@ -129,49 +173,38 @@ impl PrometheusMonitoring<CRDPrometheus> for K8sAnywhereTopology {
async fn install_prometheus( async fn install_prometheus(
&self, &self,
sender: &CRDPrometheus, sender: &CRDPrometheus,
inventory: &Inventory, _inventory: &Inventory,
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>, _receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
) -> Result<PreparationOutcome, PreparationError> { ) -> Result<PreparationOutcome, PreparationError> {
let po_result = self.ensure_prometheus_operator(sender).await?; let client = self.k8s_client().await?;
if po_result == PreparationOutcome::Noop { for monitor in sender.service_monitor.iter() {
debug!("Skipping Prometheus CR installation due to missing operator."); client
return Ok(po_result); .apply(monitor, Some(&sender.namespace))
} .await
.map_err(|e| PreparationError::new(e.to_string()))?;
let result = self
.get_k8s_prometheus_application_score(
sender.clone(),
receivers,
Some(sender.service_monitor.clone()),
)
.await
.interpret(inventory, self)
.await;
match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
details: outcome.message,
}),
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
_ => Err(PreparationError::new(outcome.message)),
},
Err(err) => Err(PreparationError::new(err.to_string())),
} }
Ok(PreparationOutcome::Success {
details: "successfuly installed prometheus components".to_string(),
})
} }
async fn ensure_prometheus_operator( async fn ensure_prometheus_operator(
&self, &self,
sender: &CRDPrometheus, sender: &CRDPrometheus,
inventory: &Inventory, _inventory: &Inventory,
) -> Result<PreparationOutcome, PreparationError> { ) -> Result<PreparationOutcome, PreparationError> {
let po_result = self.ensure_prometheus_operator(sender).await?; let po_result = self.ensure_prometheus_operator(sender).await?;
if po_result == PreparationOutcome::Noop { match po_result {
debug!("Skipping Prometheus CR installation due to missing operator."); PreparationOutcome::Success { details: _ } => {
return Ok(po_result); debug!("Detected prometheus crds operator present in cluster.");
} else { return Ok(po_result);
todo!() }
PreparationOutcome::Noop => {
debug!("Skipping Prometheus CR installation due to missing operator.");
return Ok(po_result);
}
} }
} }
} }
@ -211,6 +244,7 @@ impl PrometheusMonitoring<RHOBObservability> for K8sAnywhereTopology {
Err(err) => Err(PreparationError::new(err.to_string())), Err(err) => Err(PreparationError::new(err.to_string())),
} }
} }
async fn ensure_prometheus_operator( async fn ensure_prometheus_operator(
&self, &self,
sender: &RHOBObservability, sender: &RHOBObservability,
@ -300,6 +334,95 @@ impl K8sAnywhereTopology {
.clone() .clone()
} }
fn build_grafana_datasource(
&self,
ns: &str,
label_selector: &LabelSelector,
) -> GrafanaDatasource {
let mut json_data = BTreeMap::new();
json_data.insert("timeInterval".to_string(), "5s".to_string());
let graf_data_source = GrafanaDatasource {
metadata: ObjectMeta {
name: Some(format!("grafana-datasource-{}", ns)),
namespace: Some(ns.to_string()),
..Default::default()
},
spec: GrafanaDatasourceSpec {
instance_selector: label_selector.clone(),
allow_cross_namespace_import: Some(false),
datasource: GrafanaDatasourceConfig {
access: "proxy".to_string(),
database: Some("prometheus".to_string()),
json_data: Some(json_data),
//this is fragile
name: format!("prometheus-{}-0", ns),
r#type: "prometheus".to_string(),
url: format!("http://prometheus-operated.{}.svc.cluster.local:9090", ns),
},
},
};
graf_data_source
}
fn build_grafana_dashboard(
&self,
ns: &str,
label_selector: &LabelSelector,
) -> GrafanaDashboard {
let json = build_default_dashboard(ns);
let graf_dashboard = GrafanaDashboard {
metadata: ObjectMeta {
name: Some(format!("grafana-dashboard-{}", ns)),
namespace: Some(ns.to_string()),
..Default::default()
},
spec: GrafanaDashboardSpec {
resync_period: Some("30s".to_string()),
instance_selector: label_selector.clone(),
json,
},
};
graf_dashboard
}
fn build_grafana(&self, ns: &str, labels: &BTreeMap<String, String>) -> GrafanaCRD {
let grafana = GrafanaCRD {
metadata: ObjectMeta {
name: Some(format!("grafana-{}", ns)),
namespace: Some(ns.to_string()),
labels: Some(labels.clone()),
..Default::default()
},
spec: GrafanaSpec {
config: None,
admin_user: None,
admin_password: None,
ingress: None,
persistence: None,
resources: None,
},
};
grafana
}
async fn build_grafana_ingress(&self, ns: &str) -> K8sIngressScore {
let domain = self.get_domain(&format!("grafana-{}", ns)).await.unwrap();
let name = format!("{}-grafana", ns);
let backend_service = format!("grafana-{}-service", ns);
K8sIngressScore {
name: fqdn::fqdn!(&name),
host: fqdn::fqdn!(&domain),
backend_service: fqdn::fqdn!(&backend_service),
port: 3000,
path: Some("/".to_string()),
path_type: Some(PathType::Prefix),
namespace: Some(fqdn::fqdn!(&ns)),
ingress_class_name: Some("openshift-default".to_string()),
}
}
async fn get_cluster_observability_operator_prometheus_application_score( async fn get_cluster_observability_operator_prometheus_application_score(
&self, &self,
sender: RHOBObservability, sender: RHOBObservability,
@ -607,7 +730,14 @@ impl K8sAnywhereTopology {
inventory: &Inventory, inventory: &Inventory,
ns: Option<&str>, ns: Option<&str>,
) -> Result<PreparationOutcome, PreparationError> { ) -> Result<PreparationOutcome, PreparationError> {
let _grafana_operator_score = grafana_helm_chart_score(ns.unwrap(), true) let namespace = ns.unwrap_or("grafana");
info!("installing grafana operator in ns {namespace}");
let tenant = self.get_k8s_tenant_manager()?.get_tenant_config().await;
let mut namespace_scope = false;
if tenant.is_some() {
namespace_scope = true;
}
let _grafana_operator_score = grafana_helm_chart_score(namespace, namespace_scope)
.interpret(inventory, self) .interpret(inventory, self)
.await; .await;
Ok(PreparationOutcome::Success { Ok(PreparationOutcome::Success {

View File

@ -30,6 +30,7 @@ impl<S: AlertSender + Installable<T>, T: Topology> Interpret<T> for AlertingInte
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
debug!("hit sender configure for AlertingInterpret");
self.sender.configure(inventory, topology).await?; self.sender.configure(inventory, topology).await?;
for receiver in self.receivers.iter() { for receiver in self.receivers.iter() {
receiver.install(&self.sender).await?; receiver.install(&self.sender).await?;
@ -38,6 +39,7 @@ impl<S: AlertSender + Installable<T>, T: Topology> Interpret<T> for AlertingInte
debug!("installing rule: {:#?}", rule); debug!("installing rule: {:#?}", rule);
rule.install(&self.sender).await?; rule.install(&self.sender).await?;
} }
debug!("hit sender ensure installed for AlertingInterpret");
self.sender.ensure_installed(inventory, topology).await?; self.sender.ensure_installed(inventory, topology).await?;
Ok(Outcome::success(format!( Ok(Outcome::success(format!(
"successfully installed alert sender {}", "successfully installed alert sender {}",

View File

@ -2,7 +2,11 @@ use crate::modules::application::{
Application, ApplicationFeature, InstallationError, InstallationOutcome, 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::grafana::grafana::Grafana;
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus; use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus;
use crate::modules::monitoring::kube_prometheus::crd::service_monitor::{
ServiceMonitor, ServiceMonitorSpec,
};
use crate::topology::MultiTargetTopology; use crate::topology::MultiTargetTopology;
use crate::topology::ingress::Ingress; use crate::topology::ingress::Ingress;
use crate::{ use crate::{
@ -22,6 +26,7 @@ use base64::{Engine as _, engine::general_purpose};
use harmony_secret::SecretManager; use harmony_secret::SecretManager;
use harmony_secret_derive::Secret; use harmony_secret_derive::Secret;
use harmony_types::net::Url; use harmony_types::net::Url;
use kube::api::ObjectMeta;
use log::{debug, info}; use log::{debug, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
@ -41,6 +46,7 @@ impl<
+ K8sclient + K8sclient
+ MultiTargetTopology + MultiTargetTopology
+ PrometheusMonitoring<CRDPrometheus> + PrometheusMonitoring<CRDPrometheus>
+ Grafana
+ Ingress + Ingress
+ std::fmt::Debug, + std::fmt::Debug,
> ApplicationFeature<T> for Monitoring > ApplicationFeature<T> for Monitoring
@ -57,11 +63,20 @@ impl<
.unwrap_or_else(|| self.application.name()); .unwrap_or_else(|| self.application.name());
let domain = topology.get_domain("ntfy").await.unwrap(); let domain = topology.get_domain("ntfy").await.unwrap();
let app_service_monitor = ServiceMonitor {
metadata: ObjectMeta {
name: Some(self.application.name()),
namespace: Some(namespace.clone()),
..Default::default()
},
spec: ServiceMonitorSpec::default(),
};
let mut alerting_score = ApplicationMonitoringScore { let mut alerting_score = ApplicationMonitoringScore {
sender: CRDPrometheus { sender: CRDPrometheus {
namespace: namespace.clone(), namespace: namespace.clone(),
client: topology.k8s_client().await.unwrap(), client: topology.k8s_client().await.unwrap(),
service_monitor: vec![], service_monitor: vec![app_service_monitor],
}, },
application: self.application.clone(), application: self.application.clone(),
receivers: self.alert_receiver.clone(), receivers: self.alert_receiver.clone(),

View File

@ -1,21 +1,23 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use log::debug;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
data::Version, interpret::Interpret,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
modules::{ modules::{
application::Application, application::Application,
monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus, monitoring::{
grafana::grafana::Grafana, kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus,
},
prometheus::prometheus::PrometheusMonitoring, prometheus::prometheus::PrometheusMonitoring,
}, },
score::Score, score::Score,
topology::{PreparationOutcome, Topology, oberservability::monitoring::AlertReceiver}, topology::{
K8sclient, Topology,
oberservability::monitoring::{AlertReceiver, AlertingInterpret},
},
}; };
use harmony_types::id::Id;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct ApplicationMonitoringScore { pub struct ApplicationMonitoringScore {
@ -24,10 +26,15 @@ pub struct ApplicationMonitoringScore {
pub receivers: Vec<Box<dyn AlertReceiver<CRDPrometheus>>>, pub receivers: Vec<Box<dyn AlertReceiver<CRDPrometheus>>>,
} }
impl<T: Topology + PrometheusMonitoring<CRDPrometheus>> Score<T> for ApplicationMonitoringScore { impl<T: Topology + PrometheusMonitoring<CRDPrometheus> + K8sclient + Grafana> Score<T>
for ApplicationMonitoringScore
{
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(ApplicationMonitoringInterpret { debug!("creating alerting interpret");
score: self.clone(), Box::new(AlertingInterpret {
sender: self.sender.clone(),
receivers: self.receivers.clone(),
rules: vec![],
}) })
} }
@ -38,55 +45,3 @@ impl<T: Topology + PrometheusMonitoring<CRDPrometheus>> Score<T> for Application
) )
} }
} }
#[derive(Debug)]
pub struct ApplicationMonitoringInterpret {
score: ApplicationMonitoringScore,
}
#[async_trait]
impl<T: Topology + PrometheusMonitoring<CRDPrometheus>> Interpret<T>
for ApplicationMonitoringInterpret
{
async fn execute(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let result = topology
.install_prometheus(
&self.score.sender,
inventory,
Some(self.score.receivers.clone()),
)
.await;
match result {
Ok(outcome) => match outcome {
PreparationOutcome::Success { details: _ } => {
Ok(Outcome::success("Prometheus installed".into()))
}
PreparationOutcome::Noop => {
Ok(Outcome::noop("Prometheus installation skipped".into()))
}
},
Err(err) => Err(InterpretError::from(err)),
}
}
fn get_name(&self) -> InterpretName {
InterpretName::ApplicationMonitoring
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}

View File

@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use k8s_openapi::Resource;
use crate::{ use crate::{
inventory::Inventory, inventory::Inventory,
@ -7,9 +8,10 @@ use crate::{
#[async_trait] #[async_trait]
pub trait Grafana { pub trait Grafana {
async fn ensure_grafana_operator_ready( async fn ensure_grafana_operator(
&self, &self,
inventory: &Inventory, inventory: &Inventory,
) -> Result<PreparationOutcome, PreparationError>; ) -> Result<PreparationOutcome, PreparationError>;
async fn install_grafana(&self) -> Result<PreparationOutcome, PreparationError>; async fn install_grafana(&self) -> Result<PreparationOutcome, PreparationError>;
} }

View File

@ -1,24 +1,27 @@
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
use crate::modules::helm::chart::HelmChartScore; use crate::modules::helm::chart::{HelmChartScore, HelmRepository};
pub fn grafana_helm_chart_score(ns: &str, scope: bool) -> HelmChartScore { pub fn grafana_helm_chart_score(ns: &str, namespace_scope: bool) -> HelmChartScore {
let mut values_overrides = HashMap::new(); let mut values_overrides = HashMap::new();
values_overrides.insert( values_overrides.insert(
NonBlankString::from_str("namespaceScope").unwrap(), NonBlankString::from_str("namespaceScope").unwrap(),
scope.to_string(), namespace_scope.to_string(),
); );
HelmChartScore { HelmChartScore {
namespace: Some(NonBlankString::from_str(ns).unwrap()), namespace: Some(NonBlankString::from_str(ns).unwrap()),
release_name: NonBlankString::from_str("grafana").unwrap(), release_name: NonBlankString::from_str("grafana-operator").unwrap(),
chart_name: NonBlankString::from_str("oci://ghcr.io/grafana/helm-charts/grafana-operator") chart_name: NonBlankString::from_str("grafana/grafana-operator").unwrap(),
.unwrap(),
chart_version: None, chart_version: None,
values_overrides: Some(values_overrides), values_overrides: Some(values_overrides),
values_yaml: None, values_yaml: None,
create_namespace: true, create_namespace: true,
install_only: true, install_only: true,
repository: None, repository: Some(HelmRepository::new(
"grafana".to_string(),
url::Url::parse("https://grafana.github.io/helm-charts").unwrap(),
true,
)),
} }
} }

View File

@ -68,7 +68,7 @@ impl<T: Topology + K8sclient + PrometheusMonitoring<CRDPrometheus> + Grafana> In
for CRDPrometheus for CRDPrometheus
{ {
async fn configure(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> { async fn configure(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
topology.ensure_grafana_operator_ready(inventory).await?; topology.ensure_grafana_operator(inventory).await?;
topology.ensure_prometheus_operator(self, inventory).await?; topology.ensure_prometheus_operator(self, inventory).await?;
Ok(()) Ok(())
} }