feat: implementation of Installable for CRDPrometheusIntroduction of Grafana trait and its impl for k8sanywhereallows for CRDPrometheus to be installed via AlertingInterpret which standardizes the installation of alert receivers, alerting rules, and alert senders

This commit is contained in:
Willem 2025-10-16 14:07:23 -04:00
parent 7dff70edcf
commit fc384599a1
3 changed files with 45 additions and 93 deletions

View File

@ -1,21 +1,12 @@
use std::{
collections::{BTreeMap, HashMap},
process::Command,
sync::Arc,
};
use std::{collections::BTreeMap, process::Command, sync::Arc};
use async_trait::async_trait;
use base64::{Engine, engine::general_purpose};
use k8s_openapi::api::{
authentication::v1::{
BoundObjectReference, TokenRequest, TokenRequestSpec, TokenRequestStatus,
},
core::v1::{Secret, ServiceAccount},
core::v1::Secret,
rbac::v1::{ClusterRoleBinding, RoleRef, Subject},
};
use kube::{
Api,
api::{GroupVersionKind, ObjectMeta, PostParams},
};
use kube::api::{DynamicObject, GroupVersionKind, ObjectMeta};
use log::{debug, info, warn};
use serde::Serialize;
use tokio::sync::OnceCell;
@ -35,14 +26,11 @@ use crate::{
Grafana as GrafanaCRD, GrafanaCom, GrafanaDashboard,
GrafanaDashboardDatasource, GrafanaDashboardSpec, GrafanaDatasource,
GrafanaDatasourceConfig, GrafanaDatasourceJsonData,
GrafanaDatasourceSecureJsonData, GrafanaDatasourceSpec, GrafanaSecretKeyRef,
GrafanaSpec, GrafanaValueFrom, GrafanaValueSource,
GrafanaDatasourceSecureJsonData, GrafanaDatasourceSpec, GrafanaSpec,
},
crd_prometheuses::LabelSelector,
grafana_default_dashboard::build_default_dashboard,
prometheus_operator::prometheus_operator_helm_chart_score,
rhob_alertmanager_config::RHOBObservability,
role::build_prom_service_account,
service_monitor::ServiceMonitor,
},
},
@ -148,24 +136,23 @@ impl Grafana for K8sAnywhereTopology {
};
}
async fn install_grafana(&self) -> Result<PreparationOutcome, PreparationError> {
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![],
};
debug!("getting client");
let client = self.k8s_client().await?;
info!("creating grafanas crd");
let grafana = self.build_grafana(ns, &label);
client.apply(&grafana, Some(ns)).await?;
//TODO change this to a ensure ready or something better than just a timeout
client
.wait_until_deployment_ready(
"grafana-grafana-deployment".to_string(),
@ -175,16 +162,25 @@ impl Grafana for K8sAnywhereTopology {
.await?;
let sa_name = "grafana-grafana-sa";
let token_secret_name = "grafana-sa-token-secret";
// let sa_token_secret = self.build_sa_token_secret(token_secret_name, sa_name, ns);
//
// client.apply(&sa_token_secret, Some(ns)).await?;
let sa_token_secret = self.build_sa_token_secret(token_secret_name, sa_name, ns);
let secret = self.build_token_secret(token_secret_name, ns).await;
client.apply(&secret, Some(ns)).await?;
let token_request_status = self.create_service_account_token(sa_name, ns).await?;
client.apply(&sa_token_secret, Some(ns)).await?;
let secret_gvk = GroupVersionKind {
group: "".to_string(),
version: "v1".to_string(),
kind: "Secret".to_string(),
};
let secret = client
.get_resource_json_value(token_secret_name, Some(ns), &secret_gvk)
.await?;
let token = format!(
"Bearer {}",
self.extract_and_normalize_token(&secret).unwrap()
);
debug!("creating grafana clusterrole binding");
@ -195,8 +191,6 @@ impl Grafana for K8sAnywhereTopology {
debug!("creating grafana datasource crd");
// let token_str = format!("Bearer {}", token.token);
let thanos_url = format!(
"https://{}",
self.get_domain("thanos-querier-openshift-monitoring")
@ -209,7 +203,7 @@ impl Grafana for K8sAnywhereTopology {
ns,
&label_selector,
&thanos_url,
&token_request_status.token, // Pass the secret name here
&token,
);
client.apply(&thanos_openshift_datasource, Some(ns)).await?;
@ -398,8 +392,21 @@ impl K8sAnywhereTopology {
.clone()
}
pub fn build_service_account(&self, name: &str, namespace: &str) -> ServiceAccount {
build_prom_service_account(name.to_string(), namespace.to_string())
fn extract_and_normalize_token(&self, secret: &DynamicObject) -> Option<String> {
let token_b64 = secret
.data
.get("token")
.or_else(|| secret.data.get("data").and_then(|d| d.get("token")))
.and_then(|v| v.as_str())?;
let bytes = general_purpose::STANDARD.decode(token_b64).ok()?;
let s = String::from_utf8(bytes).ok()?;
let cleaned = s
.trim_matches(|c: char| c.is_whitespace() || c == '\0')
.to_string();
Some(cleaned)
}
pub fn build_cluster_rolebinding(
@ -451,69 +458,13 @@ impl K8sAnywhereTopology {
}
}
pub fn get_token_request(&self, ns: &str) -> TokenRequest {
debug!("building token request");
TokenRequest {
metadata: ObjectMeta {
namespace: Some(ns.to_string()),
..Default::default()
},
spec: TokenRequestSpec {
audiences: vec!["https://kubernetes.default.svc".to_string()],
expiration_seconds: Some(3600),
bound_object_ref: Some(BoundObjectReference {
kind: Some("Secret".to_string()),
name: Some("grafana-sa-token-secret".to_string()),
..Default::default()
}),
},
..Default::default()
}
}
pub async fn create_service_account_token(
&self,
service_account_name: &str,
ns: &str,
) -> Result<TokenRequestStatus, PreparationError> {
debug!("creating service account token");
let token_request = self.get_token_request(ns);
let client = self.k8s_client().await?;
let pp = PostParams::default();
let token_requests_api = client.service_account_api(ns).await;
let data = serde_json::to_vec(&token_request).unwrap();
let created_token_request = token_requests_api
.create_subresource::<TokenRequest>("token", service_account_name, &pp, data)
.await?;
let status = created_token_request
.status
.ok_or_else(|| PreparationError::new("missing token request status".to_string()))?;
Ok(status)
}
pub async fn build_token_secret(&self, secret_name: &str, ns: &str) -> Secret {
Secret {
metadata: ObjectMeta {
name: Some(secret_name.into()),
namespace: Some(ns.into()),
..Default::default()
},
string_data: None,
..Default::default()
}
}
fn build_grafana_datasource(
&self,
name: &str,
ns: &str,
label_selector: &LabelSelector,
url: &str,
token: &str, // Pass in the secret name
token: &str,
) -> GrafanaDatasource {
let mut json_data = BTreeMap::new();
json_data.insert("timeInterval".to_string(), "5s".to_string());

View File

@ -105,7 +105,7 @@ pub struct GrafanaDashboardSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub datasources: Option<Vec<GrafanaDashboardDatasource>>,
pub instance_selector: LabelSelector,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -156,7 +156,6 @@ pub struct GrafanaDatasourceSpec {
pub values_from: Option<Vec<GrafanaValueFrom>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaValueFrom {

View File

@ -11,7 +11,9 @@ use std::process::Command;
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus;
use crate::modules::monitoring::kube_prometheus::crd::crd_default_rules::build_default_application_rules;
use crate::modules::monitoring::kube_prometheus::crd::crd_grafana::{
Grafana, GrafanaDashboard, GrafanaDashboardSpec, GrafanaDatasource, GrafanaDatasourceConfig, GrafanaDatasourceJsonData, GrafanaDatasourceSpec, GrafanaSecretKeyRef, GrafanaSpec, GrafanaValueFrom, GrafanaValueSource
Grafana, GrafanaDashboard, GrafanaDashboardSpec, GrafanaDatasource, GrafanaDatasourceConfig,
GrafanaDatasourceJsonData, GrafanaDatasourceSpec, GrafanaSecretKeyRef, GrafanaSpec,
GrafanaValueFrom, GrafanaValueSource,
};
use crate::modules::monitoring::kube_prometheus::crd::crd_prometheus_rules::{
PrometheusRule, PrometheusRuleSpec, RuleGroup,