Compare commits
	
		
			No commits in common. "935ecd3f95e5bb9eaa2986dae236bd2fd1c1efb6" and "61193556810afbd719e8dc2858705da468871c72" have entirely different histories.
		
	
	
		
			935ecd3f95
			...
			6119355681
		
	
		
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +0,0 @@ | |||||||
| [submodule "examples/try_rust_webapp/tryrust.org"] |  | ||||||
| 	path = examples/try_rust_webapp/tryrust.org |  | ||||||
| 	url = https://github.com/rust-dd/tryrust.org.git |  | ||||||
							
								
								
									
										59
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1838,21 +1838,6 @@ dependencies = [ | |||||||
|  "url", |  "url", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "example-try-rust-webapp" |  | ||||||
| version = "0.1.0" |  | ||||||
| dependencies = [ |  | ||||||
|  "base64 0.22.1", |  | ||||||
|  "env_logger", |  | ||||||
|  "harmony", |  | ||||||
|  "harmony_cli", |  | ||||||
|  "harmony_macros", |  | ||||||
|  "harmony_types", |  | ||||||
|  "log", |  | ||||||
|  "tokio", |  | ||||||
|  "url", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "example-tui" | name = "example-tui" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| @ -2333,7 +2318,6 @@ dependencies = [ | |||||||
|  "tokio-util", |  "tokio-util", | ||||||
|  "url", |  "url", | ||||||
|  "uuid", |  "uuid", | ||||||
|  "walkdir", |  | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -4632,21 +4616,6 @@ dependencies = [ | |||||||
|  "subtle", |  "subtle", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "rhob-application-monitoring" |  | ||||||
| version = "0.1.0" |  | ||||||
| dependencies = [ |  | ||||||
|  "base64 0.22.1", |  | ||||||
|  "env_logger", |  | ||||||
|  "harmony", |  | ||||||
|  "harmony_cli", |  | ||||||
|  "harmony_macros", |  | ||||||
|  "harmony_types", |  | ||||||
|  "log", |  | ||||||
|  "tokio", |  | ||||||
|  "url", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "ring" | name = "ring" | ||||||
| version = "0.17.14" | version = "0.17.14" | ||||||
| @ -4987,15 +4956,6 @@ dependencies = [ | |||||||
|  "cipher", |  "cipher", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "same-file" |  | ||||||
| version = "1.0.6" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" |  | ||||||
| dependencies = [ |  | ||||||
|  "winapi-util", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "schannel" | name = "schannel" | ||||||
| version = "0.1.27" | version = "0.1.27" | ||||||
| @ -6535,16 +6495,6 @@ dependencies = [ | |||||||
|  "libc", |  "libc", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "walkdir" |  | ||||||
| version = "2.5.0" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" |  | ||||||
| dependencies = [ |  | ||||||
|  "same-file", |  | ||||||
|  "winapi-util", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "want" | name = "want" | ||||||
| version = "0.3.1" | version = "0.3.1" | ||||||
| @ -6727,15 +6677,6 @@ version = "0.4.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "winapi-util" |  | ||||||
| version = "0.1.10" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" |  | ||||||
| dependencies = [ |  | ||||||
|  "windows-sys 0.60.2", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "winapi-x86_64-pc-windows-gnu" | name = "winapi-x86_64-pc-windows-gnu" | ||||||
| version = "0.4.0" | version = "0.4.0" | ||||||
|  | |||||||
| @ -30,7 +30,6 @@ async fn main() { | |||||||
|         domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), |         domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), | ||||||
|         project_root: PathBuf::from("./examples/rust/webapp"), |         project_root: PathBuf::from("./examples/rust/webapp"), | ||||||
|         framework: Some(RustWebFramework::Leptos), |         framework: Some(RustWebFramework::Leptos), | ||||||
|         service_port: 3000, |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     let webhook_receiver = WebhookReceiver { |     let webhook_receiver = WebhookReceiver { | ||||||
|  | |||||||
| @ -1,17 +0,0 @@ | |||||||
| [package] |  | ||||||
| name = "rhob-application-monitoring" |  | ||||||
| edition = "2024" |  | ||||||
| version.workspace = true |  | ||||||
| readme.workspace = true |  | ||||||
| license.workspace = true |  | ||||||
| 
 |  | ||||||
| [dependencies] |  | ||||||
| harmony = { path = "../../harmony" } |  | ||||||
| harmony_cli = { path = "../../harmony_cli" } |  | ||||||
| harmony_types = { path = "../../harmony_types" } |  | ||||||
| harmony_macros = { path = "../../harmony_macros" } |  | ||||||
| tokio = { workspace = true } |  | ||||||
| log = { workspace = true } |  | ||||||
| env_logger = { workspace = true } |  | ||||||
| url = { workspace = true } |  | ||||||
| base64.workspace = true |  | ||||||
| @ -1,50 +0,0 @@ | |||||||
| use std::{path::PathBuf, sync::Arc}; |  | ||||||
| 
 |  | ||||||
| use harmony::{ |  | ||||||
|     inventory::Inventory, |  | ||||||
|     modules::{ |  | ||||||
|         application::{ |  | ||||||
|             ApplicationScore, RustWebFramework, RustWebapp, |  | ||||||
|             features::rhob_monitoring::RHOBMonitoring, |  | ||||||
|         }, |  | ||||||
|         monitoring::alert_channel::discord_alert_channel::DiscordWebhook, |  | ||||||
|     }, |  | ||||||
|     topology::K8sAnywhereTopology, |  | ||||||
| }; |  | ||||||
| use harmony_types::net::Url; |  | ||||||
| 
 |  | ||||||
| #[tokio::main] |  | ||||||
| async fn main() { |  | ||||||
|     let application = Arc::new(RustWebapp { |  | ||||||
|         name: "test-rhob-monitoring".to_string(), |  | ||||||
|         domain: Url::Url(url::Url::parse("htps://some-fake-url").unwrap()), |  | ||||||
|         project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
 |  | ||||||
|         framework: Some(RustWebFramework::Leptos), |  | ||||||
|         service_port: 3000, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     let discord_receiver = DiscordWebhook { |  | ||||||
|         name: "test-discord".to_string(), |  | ||||||
|         url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()), |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     let app = ApplicationScore { |  | ||||||
|         features: vec![ |  | ||||||
|             Box::new(RHOBMonitoring { |  | ||||||
|                 application: application.clone(), |  | ||||||
|                 alert_receiver: vec![Box::new(discord_receiver)], |  | ||||||
|             }), |  | ||||||
|             // TODO add backups, multisite ha, etc
 |  | ||||||
|         ], |  | ||||||
|         application, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     harmony_cli::run( |  | ||||||
|         Inventory::autoload(), |  | ||||||
|         K8sAnywhereTopology::from_env(), |  | ||||||
|         vec![Box::new(app)], |  | ||||||
|         None, |  | ||||||
|     ) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
| } |  | ||||||
| @ -13,26 +13,25 @@ use harmony::{ | |||||||
|     }, |     }, | ||||||
|     topology::K8sAnywhereTopology, |     topology::K8sAnywhereTopology, | ||||||
| }; | }; | ||||||
| use harmony_macros::hurl; | use harmony_macros::remote_url; | ||||||
| 
 | 
 | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| async fn main() { | async fn main() { | ||||||
|     let application = Arc::new(RustWebapp { |     let application = Arc::new(RustWebapp { | ||||||
|         name: "harmony-example-rust-webapp".to_string(), |         name: "harmony-example-rust-webapp".to_string(), | ||||||
|         domain: hurl!("https://rustapp.harmony.example.com"), |         domain: remote_url!("https://rustapp.harmony.example.com"), | ||||||
|         project_root: PathBuf::from("./webapp"), |         project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
 | ||||||
|         framework: Some(RustWebFramework::Leptos), |         framework: Some(RustWebFramework::Leptos), | ||||||
|         service_port: 3000, |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     let discord_receiver = DiscordWebhook { |     let discord_receiver = DiscordWebhook { | ||||||
|         name: "test-discord".to_string(), |         name: "test-discord".to_string(), | ||||||
|         url: hurl!("https://discord.doesnt.exist.com"), |         url: remote_url!("https://discord.doesnt.exist.com"), | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let webhook_receiver = WebhookReceiver { |     let webhook_receiver = WebhookReceiver { | ||||||
|         name: "sample-webhook-receiver".to_string(), |         name: "sample-webhook-receiver".to_string(), | ||||||
|         url: hurl!("https://webhook-doesnt-exist.com"), |         url: remote_url!("https://webhook-doesnt-exist.com"), | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let app = ApplicationScore { |     let app = ApplicationScore { | ||||||
|  | |||||||
| @ -1,17 +0,0 @@ | |||||||
| [package] |  | ||||||
| name = "example-try-rust-webapp" |  | ||||||
| edition = "2024" |  | ||||||
| version.workspace = true |  | ||||||
| readme.workspace = true |  | ||||||
| license.workspace = true |  | ||||||
| 
 |  | ||||||
| [dependencies] |  | ||||||
| harmony = { path = "../../harmony" } |  | ||||||
| harmony_cli = { path = "../../harmony_cli" } |  | ||||||
| harmony_types = { path = "../../harmony_types" } |  | ||||||
| harmony_macros = { path = "../../harmony_macros" } |  | ||||||
| tokio = { workspace = true } |  | ||||||
| log = { workspace = true } |  | ||||||
| env_logger = { workspace = true } |  | ||||||
| url = { workspace = true } |  | ||||||
| base64.workspace = true |  | ||||||
| @ -1,52 +0,0 @@ | |||||||
| use std::{path::PathBuf, sync::Arc}; |  | ||||||
| 
 |  | ||||||
| use harmony::{ |  | ||||||
|     inventory::Inventory, |  | ||||||
|     modules::{ |  | ||||||
|         application::{ |  | ||||||
|             ApplicationScore, RustWebFramework, RustWebapp, |  | ||||||
|             features::{ContinuousDelivery, Monitoring}, |  | ||||||
|         }, |  | ||||||
|         monitoring::alert_channel::discord_alert_channel::DiscordWebhook, |  | ||||||
|     }, |  | ||||||
|     topology::K8sAnywhereTopology, |  | ||||||
| }; |  | ||||||
| use harmony_types::net::Url; |  | ||||||
| 
 |  | ||||||
| #[tokio::main] |  | ||||||
| async fn main() { |  | ||||||
|     let application = Arc::new(RustWebapp { |  | ||||||
|         name: "harmony-example-tryrust".to_string(), |  | ||||||
|         domain: Url::Url(url::Url::parse("https://tryrust.harmony.example.com").unwrap()), |  | ||||||
|         project_root: PathBuf::from("./tryrust.org"), |  | ||||||
|         framework: Some(RustWebFramework::Leptos), |  | ||||||
|         service_port: 8080, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     let discord_receiver = DiscordWebhook { |  | ||||||
|         name: "test-discord".to_string(), |  | ||||||
|         url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()), |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     let app = ApplicationScore { |  | ||||||
|         features: vec![ |  | ||||||
|             Box::new(ContinuousDelivery { |  | ||||||
|                 application: application.clone(), |  | ||||||
|             }), |  | ||||||
|             Box::new(Monitoring { |  | ||||||
|                 application: application.clone(), |  | ||||||
|                 alert_receiver: vec![Box::new(discord_receiver)], |  | ||||||
|             }), |  | ||||||
|         ], |  | ||||||
|         application, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     harmony_cli::run( |  | ||||||
|         Inventory::autoload(), |  | ||||||
|         K8sAnywhereTopology::from_env(), |  | ||||||
|         vec![Box::new(app)], |  | ||||||
|         None, |  | ||||||
|     ) |  | ||||||
|     .await |  | ||||||
|     .unwrap(); |  | ||||||
| } |  | ||||||
| @ -1 +0,0 @@ | |||||||
| Subproject commit 0f9ba145172867f467e5320b37d07a5bbb7dd438 |  | ||||||
| @ -66,7 +66,6 @@ tar.workspace = true | |||||||
| base64.workspace = true | base64.workspace = true | ||||||
| thiserror.workspace = true | thiserror.workspace = true | ||||||
| once_cell = "1.21.3" | once_cell = "1.21.3" | ||||||
| walkdir = "2.5.0" |  | ||||||
| harmony_inventory_agent = { path = "../harmony_inventory_agent" } | harmony_inventory_agent = { path = "../harmony_inventory_agent" } | ||||||
| harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" } | harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" } | ||||||
| askama.workspace = true | askama.workspace = true | ||||||
|  | |||||||
| @ -32,7 +32,6 @@ pub enum InterpretName { | |||||||
|     K8sPrometheusCrdAlerting, |     K8sPrometheusCrdAlerting, | ||||||
|     DiscoverInventoryAgent, |     DiscoverInventoryAgent, | ||||||
|     CephClusterHealth, |     CephClusterHealth, | ||||||
|     RHOBAlerting, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl std::fmt::Display for InterpretName { | impl std::fmt::Display for InterpretName { | ||||||
| @ -61,7 +60,6 @@ impl std::fmt::Display for InterpretName { | |||||||
|             InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"), |             InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"), | ||||||
|             InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"), |             InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"), | ||||||
|             InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"), |             InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"), | ||||||
|             InterpretName::RHOBAlerting => f.write_str("RHOBAlerting"), |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ use kube::{ | |||||||
| }; | }; | ||||||
| use log::{debug, error, trace}; | use log::{debug, error, trace}; | ||||||
| use serde::{Serialize, de::DeserializeOwned}; | use serde::{Serialize, de::DeserializeOwned}; | ||||||
| use serde_json::{Value, json}; | use serde_json::json; | ||||||
| use similar::TextDiff; | use similar::TextDiff; | ||||||
| use tokio::io::AsyncReadExt; | use tokio::io::AsyncReadExt; | ||||||
| 
 | 
 | ||||||
| @ -53,21 +53,6 @@ impl K8sClient { | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn get_resource_json_value( |  | ||||||
|         &self, |  | ||||||
|         name: &str, |  | ||||||
|         namespace: Option<&str>, |  | ||||||
|         gvk: &GroupVersionKind, |  | ||||||
|     ) -> Result<DynamicObject, Error> { |  | ||||||
|         let gvk = ApiResource::from_gvk(gvk); |  | ||||||
|         let resource: Api<DynamicObject> = if let Some(ns) = namespace { |  | ||||||
|             Api::namespaced_with(self.client.clone(), ns, &gvk) |  | ||||||
|         } else { |  | ||||||
|             Api::default_namespaced_with(self.client.clone(), &gvk) |  | ||||||
|         }; |  | ||||||
|         Ok(resource.get(name).await?) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub async fn get_deployment( |     pub async fn get_deployment( | ||||||
|         &self, |         &self, | ||||||
|         name: &str, |         name: &str, | ||||||
|  | |||||||
| @ -14,11 +14,10 @@ use crate::{ | |||||||
|         monitoring::kube_prometheus::crd::{ |         monitoring::kube_prometheus::crd::{ | ||||||
|             crd_alertmanager_config::CRDPrometheus, |             crd_alertmanager_config::CRDPrometheus, | ||||||
|             prometheus_operator::prometheus_operator_helm_chart_score, |             prometheus_operator::prometheus_operator_helm_chart_score, | ||||||
|             rhob_alertmanager_config::RHOBObservability, |  | ||||||
|         }, |         }, | ||||||
|         prometheus::{ |         prometheus::{ | ||||||
|             k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore, |             k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore, | ||||||
|             prometheus::PrometheusApplicationMonitoring, rhob_alerting_score::RHOBAlertingScore, |             prometheus::PrometheusApplicationMonitoring, | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
|     score::Score, |     score::Score, | ||||||
| @ -109,43 +108,6 @@ impl PrometheusApplicationMonitoring<CRDPrometheus> for K8sAnywhereTopology { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] |  | ||||||
| impl PrometheusApplicationMonitoring<RHOBObservability> for K8sAnywhereTopology { |  | ||||||
|     async fn install_prometheus( |  | ||||||
|         &self, |  | ||||||
|         sender: &RHOBObservability, |  | ||||||
|         inventory: &Inventory, |  | ||||||
|         receivers: Option<Vec<Box<dyn AlertReceiver<RHOBObservability>>>>, |  | ||||||
|     ) -> Result<PreparationOutcome, PreparationError> { |  | ||||||
|         let po_result = self.ensure_cluster_observability_operator(sender).await?; |  | ||||||
| 
 |  | ||||||
|         if po_result == PreparationOutcome::Noop { |  | ||||||
|             debug!("Skipping Prometheus CR installation due to missing operator."); |  | ||||||
|             return Ok(po_result); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let result = self |  | ||||||
|             .get_cluster_observability_operator_prometheus_application_score( |  | ||||||
|                 sender.clone(), |  | ||||||
|                 receivers, |  | ||||||
|             ) |  | ||||||
|             .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())), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Serialize for K8sAnywhereTopology { | impl Serialize for K8sAnywhereTopology { | ||||||
|     fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> |     fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> | ||||||
|     where |     where | ||||||
| @ -172,19 +134,6 @@ impl K8sAnywhereTopology { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn get_cluster_observability_operator_prometheus_application_score( |  | ||||||
|         &self, |  | ||||||
|         sender: RHOBObservability, |  | ||||||
|         receivers: Option<Vec<Box<dyn AlertReceiver<RHOBObservability>>>>, |  | ||||||
|     ) -> RHOBAlertingScore { |  | ||||||
|         RHOBAlertingScore { |  | ||||||
|             sender, |  | ||||||
|             receivers: receivers.unwrap_or_default(), |  | ||||||
|             service_monitors: vec![], |  | ||||||
|             prometheus_rules: vec![], |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn get_k8s_prometheus_application_score( |     async fn get_k8s_prometheus_application_score( | ||||||
|         &self, |         &self, | ||||||
|         sender: CRDPrometheus, |         sender: CRDPrometheus, | ||||||
| @ -337,60 +286,6 @@ impl K8sAnywhereTopology { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn ensure_cluster_observability_operator( |  | ||||||
|         &self, |  | ||||||
|         sender: &RHOBObservability, |  | ||||||
|     ) -> Result<PreparationOutcome, PreparationError> { |  | ||||||
|         let status = Command::new("sh") |  | ||||||
|             .args(["-c", "kubectl get crd -A | grep -i rhobs"]) |  | ||||||
|             .status() |  | ||||||
|             .map_err(|e| PreparationError::new(format!("could not connect to cluster: {}", e)))?; |  | ||||||
| 
 |  | ||||||
|         if !status.success() { |  | ||||||
|             if let Some(Some(k8s_state)) = self.k8s_state.get() { |  | ||||||
|                 match k8s_state.source { |  | ||||||
|                     K8sSource::LocalK3d => { |  | ||||||
|                         debug!("installing cluster observability operator"); |  | ||||||
|                         todo!(); |  | ||||||
|                         let op_score = |  | ||||||
|                             prometheus_operator_helm_chart_score(sender.namespace.clone()); |  | ||||||
|                         let result = op_score.interpret(&Inventory::empty(), self).await; |  | ||||||
| 
 |  | ||||||
|                         return match result { |  | ||||||
|                             Ok(outcome) => match outcome.status { |  | ||||||
|                                 InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success { |  | ||||||
|                                     details: "installed cluster observability operator".into(), |  | ||||||
|                                 }), |  | ||||||
|                                 InterpretStatus::NOOP => Ok(PreparationOutcome::Noop), |  | ||||||
|                                 _ => Err(PreparationError::new( |  | ||||||
|                                     "failed to install cluster observability operator (unknown error)".into(), |  | ||||||
|                                 )), |  | ||||||
|                             }, |  | ||||||
|                             Err(err) => Err(PreparationError::new(err.to_string())), |  | ||||||
|                         }; |  | ||||||
|                     } |  | ||||||
|                     K8sSource::Kubeconfig => { |  | ||||||
|                         debug!( |  | ||||||
|                             "unable to install cluster observability operator, contact cluster admin" |  | ||||||
|                         ); |  | ||||||
|                         return Ok(PreparationOutcome::Noop); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 warn!( |  | ||||||
|                     "Unable to detect k8s_state. Skipping Cluster Observability Operator install." |  | ||||||
|                 ); |  | ||||||
|                 return Ok(PreparationOutcome::Noop); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         debug!("Cluster Observability Operator is already present, skipping install"); |  | ||||||
| 
 |  | ||||||
|         Ok(PreparationOutcome::Success { |  | ||||||
|             details: "cluster observability operator present in cluster".into(), |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn ensure_prometheus_operator( |     async fn ensure_prometheus_operator( | ||||||
|         &self, |         &self, | ||||||
|         sender: &CRDPrometheus, |         sender: &CRDPrometheus, | ||||||
|  | |||||||
| @ -176,18 +176,18 @@ impl< | |||||||
|             } |             } | ||||||
|             target => { |             target => { | ||||||
|                 info!("Deploying {} to target {target:?}", self.application.name()); |                 info!("Deploying {} to target {target:?}", self.application.name()); | ||||||
| 
 |  | ||||||
|                 let score = ArgoHelmScore { |                 let score = ArgoHelmScore { | ||||||
|                     namespace: format!("{}", self.application.name()), |                     namespace: "harmony-example-rust-webapp".to_string(), | ||||||
|                     openshift: true, |                     openshift: true, | ||||||
|  |                     domain: "argo.harmonydemo.apps.ncd0.harmony.mcd".to_string(), | ||||||
|                     argo_apps: vec![ArgoApplication::from(CDApplicationConfig { |                     argo_apps: vec![ArgoApplication::from(CDApplicationConfig { | ||||||
|                         // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
 |                         // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
 | ||||||
|                         version: Version::from("0.1.0").unwrap(), |                         version: Version::from("0.1.0").unwrap(), | ||||||
|                         helm_chart_repo_url: "hub.nationtech.io/harmony".to_string(), |                         helm_chart_repo_url: "hub.nationtech.io/harmony".to_string(), | ||||||
|                         helm_chart_name: format!("{}-chart", self.application.name()), |                         helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), | ||||||
|                         values_overrides: None, |                         values_overrides: None, | ||||||
|                         name: format!("{}", self.application.name()), |                         name: "harmony-demo-rust-webapp".to_string(), | ||||||
|                         namespace: format!("{}", self.application.name()), |                         namespace: "harmony-example-rust-webapp".to_string(), | ||||||
|                     })], |                     })], | ||||||
|                 }; |                 }; | ||||||
|                 score |                 score | ||||||
|  | |||||||
| @ -1,10 +1,7 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use kube::{Api, api::GroupVersionKind}; |  | ||||||
| use log::{debug, warn}; |  | ||||||
| use non_blank_string_rs::NonBlankString; | use non_blank_string_rs::NonBlankString; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use serde::de::DeserializeOwned; | use std::str::FromStr; | ||||||
| use std::{process::Command, str::FromStr, sync::Arc}; |  | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     data::Version, |     data::Version, | ||||||
| @ -12,9 +9,7 @@ use crate::{ | |||||||
|     inventory::Inventory, |     inventory::Inventory, | ||||||
|     modules::helm::chart::{HelmChartScore, HelmRepository}, |     modules::helm::chart::{HelmChartScore, HelmRepository}, | ||||||
|     score::Score, |     score::Score, | ||||||
|     topology::{ |     topology::{HelmCommand, K8sclient, Topology}, | ||||||
|         HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, k8s::K8sClient, |  | ||||||
|     }, |  | ||||||
| }; | }; | ||||||
| use harmony_types::id::Id; | use harmony_types::id::Id; | ||||||
| 
 | 
 | ||||||
| @ -24,13 +19,15 @@ use super::ArgoApplication; | |||||||
| pub struct ArgoHelmScore { | pub struct ArgoHelmScore { | ||||||
|     pub namespace: String, |     pub namespace: String, | ||||||
|     pub openshift: bool, |     pub openshift: bool, | ||||||
|  |     pub domain: String, | ||||||
|     pub argo_apps: Vec<ArgoApplication>, |     pub argo_apps: Vec<ArgoApplication>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore { | impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore { | ||||||
|     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { |     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { | ||||||
|  |         let helm_score = argo_helm_chart_score(&self.namespace, self.openshift, &self.domain); | ||||||
|         Box::new(ArgoInterpret { |         Box::new(ArgoInterpret { | ||||||
|             score: self.clone(), |             score: helm_score, | ||||||
|             argo_apps: self.argo_apps.clone(), |             argo_apps: self.argo_apps.clone(), | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| @ -42,7 +39,7 @@ impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore { | |||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct ArgoInterpret { | pub struct ArgoInterpret { | ||||||
|     score: ArgoHelmScore, |     score: HelmChartScore, | ||||||
|     argo_apps: Vec<ArgoApplication>, |     argo_apps: Vec<ArgoApplication>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -53,16 +50,9 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret { | |||||||
|         inventory: &Inventory, |         inventory: &Inventory, | ||||||
|         topology: &T, |         topology: &T, | ||||||
|     ) -> Result<Outcome, InterpretError> { |     ) -> Result<Outcome, InterpretError> { | ||||||
|  |         self.score.interpret(inventory, topology).await?; | ||||||
|  | 
 | ||||||
|         let k8s_client = topology.k8s_client().await?; |         let k8s_client = topology.k8s_client().await?; | ||||||
|         let domain = self |  | ||||||
|             .get_host_domain(k8s_client.clone(), self.score.openshift) |  | ||||||
|             .await?; |  | ||||||
|         let domain = format!("argo.{domain}"); |  | ||||||
|         let helm_score = |  | ||||||
|             argo_helm_chart_score(&self.score.namespace, self.score.openshift, &domain); |  | ||||||
| 
 |  | ||||||
|         helm_score.interpret(inventory, topology).await?; |  | ||||||
| 
 |  | ||||||
|         k8s_client |         k8s_client | ||||||
|             .apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None) |             .apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None) | ||||||
|             .await |             .await | ||||||
| @ -95,38 +85,6 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl ArgoInterpret { |  | ||||||
|     pub async fn get_host_domain( |  | ||||||
|         &self, |  | ||||||
|         client: Arc<K8sClient>, |  | ||||||
|         openshift: bool, |  | ||||||
|     ) -> Result<String, InterpretError> { |  | ||||||
|         //This should be the job of the topology to determine if we are in
 |  | ||||||
|         //openshift, potentially we need on openshift topology the same way we create a
 |  | ||||||
|         //localhosttopology
 |  | ||||||
|         match openshift { |  | ||||||
|             true => { |  | ||||||
|                 let gvk = GroupVersionKind { |  | ||||||
|                     group: "operator.openshift.io".into(), |  | ||||||
|                     version: "v1".into(), |  | ||||||
|                     kind: "IngressController".into(), |  | ||||||
|                 }; |  | ||||||
|                 let ic = client |  | ||||||
|                     .get_resource_json_value("default", Some("openshift-ingress-operator"), &gvk) |  | ||||||
|                     .await?; |  | ||||||
| 
 |  | ||||||
|                 match ic.data["status"]["domain"].as_str() { |  | ||||||
|                     Some(domain) => return Ok(domain.to_string()), |  | ||||||
|                     None => return Err(InterpretError::new("Could not find domain".to_string())), |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             false => { |  | ||||||
|                 todo!() |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub fn argo_helm_chart_score(namespace: &str, openshift: bool, domain: &str) -> HelmChartScore { | pub fn argo_helm_chart_score(namespace: &str, openshift: bool, domain: &str) -> HelmChartScore { | ||||||
|     let values = format!( |     let values = format!( | ||||||
|         r#" |         r#" | ||||||
| @ -702,7 +660,7 @@ server: | |||||||
|       # nginx.ingress.kubernetes.io/ssl-passthrough: "true" |       # nginx.ingress.kubernetes.io/ssl-passthrough: "true" | ||||||
| 
 | 
 | ||||||
|     # -- Defines which ingress controller will implement the resource |     # -- Defines which ingress controller will implement the resource | ||||||
|     ingressClassName: "openshift-default" |     ingressClassName: "" | ||||||
| 
 | 
 | ||||||
|     # -- Argo CD server hostname |     # -- Argo CD server hostname | ||||||
|     # @default -- `""` (defaults to global.domain) |     # @default -- `""` (defaults to global.domain) | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| mod endpoint; | mod endpoint; | ||||||
| pub mod rhob_monitoring; |  | ||||||
| pub use endpoint::*; | pub use endpoint::*; | ||||||
| 
 | 
 | ||||||
| mod monitoring; | mod monitoring; | ||||||
|  | |||||||
| @ -1,109 +0,0 @@ | |||||||
| use std::sync::Arc; |  | ||||||
| 
 |  | ||||||
| use crate::modules::application::{Application, ApplicationFeature}; |  | ||||||
| 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::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability; |  | ||||||
| use crate::topology::MultiTargetTopology; |  | ||||||
| use crate::{ |  | ||||||
|     inventory::Inventory, |  | ||||||
|     modules::monitoring::{ |  | ||||||
|         alert_channel::webhook_receiver::WebhookReceiver, ntfy::ntfy::NtfyScore, |  | ||||||
|     }, |  | ||||||
|     score::Score, |  | ||||||
|     topology::{HelmCommand, K8sclient, Topology, tenant::TenantManager}, |  | ||||||
| }; |  | ||||||
| use crate::{ |  | ||||||
|     modules::prometheus::prometheus::PrometheusApplicationMonitoring, |  | ||||||
|     topology::oberservability::monitoring::AlertReceiver, |  | ||||||
| }; |  | ||||||
| use async_trait::async_trait; |  | ||||||
| use base64::{Engine as _, engine::general_purpose}; |  | ||||||
| use harmony_types::net::Url; |  | ||||||
| use log::{debug, info}; |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone)] |  | ||||||
| pub struct RHOBMonitoring { |  | ||||||
|     pub application: Arc<dyn Application>, |  | ||||||
|     pub alert_receiver: Vec<Box<dyn AlertReceiver<RHOBObservability>>>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[async_trait] |  | ||||||
| impl< |  | ||||||
|     T: Topology |  | ||||||
|         + HelmCommand |  | ||||||
|         + 'static |  | ||||||
|         + TenantManager |  | ||||||
|         + K8sclient |  | ||||||
|         + MultiTargetTopology |  | ||||||
|         + std::fmt::Debug |  | ||||||
|         + PrometheusApplicationMonitoring<RHOBObservability>, |  | ||||||
| > ApplicationFeature<T> for RHOBMonitoring |  | ||||||
| { |  | ||||||
|     async fn ensure_installed(&self, topology: &T) -> Result<(), String> { |  | ||||||
|         info!("Ensuring monitoring is available for application"); |  | ||||||
|         let namespace = topology |  | ||||||
|             .get_tenant_config() |  | ||||||
|             .await |  | ||||||
|             .map(|ns| ns.name.clone()) |  | ||||||
|             .unwrap_or_else(|| self.application.name()); |  | ||||||
| 
 |  | ||||||
|         let mut alerting_score = ApplicationRHOBMonitoringScore { |  | ||||||
|             sender: RHOBObservability { |  | ||||||
|                 namespace: namespace.clone(), |  | ||||||
|                 client: topology.k8s_client().await.unwrap(), |  | ||||||
|             }, |  | ||||||
|             application: self.application.clone(), |  | ||||||
|             receivers: self.alert_receiver.clone(), |  | ||||||
|         }; |  | ||||||
|         let ntfy = NtfyScore { |  | ||||||
|             namespace: namespace.clone(), |  | ||||||
|             host: "ntfy.harmonydemo.apps.ncd0.harmony.mcd".to_string(), |  | ||||||
|         }; |  | ||||||
|         ntfy.interpret(&Inventory::empty(), topology) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| e.to_string())?; |  | ||||||
| 
 |  | ||||||
|         let ntfy_default_auth_username = "harmony"; |  | ||||||
|         let ntfy_default_auth_password = "harmony"; |  | ||||||
|         let ntfy_default_auth_header = format!( |  | ||||||
|             "Basic {}", |  | ||||||
|             general_purpose::STANDARD.encode(format!( |  | ||||||
|                 "{ntfy_default_auth_username}:{ntfy_default_auth_password}" |  | ||||||
|             )) |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         debug!("ntfy_default_auth_header: {ntfy_default_auth_header}"); |  | ||||||
| 
 |  | ||||||
|         let ntfy_default_auth_param = general_purpose::STANDARD |  | ||||||
|             .encode(ntfy_default_auth_header) |  | ||||||
|             .replace("=", ""); |  | ||||||
| 
 |  | ||||||
|         debug!("ntfy_default_auth_param: {ntfy_default_auth_param}"); |  | ||||||
| 
 |  | ||||||
|         let ntfy_receiver = WebhookReceiver { |  | ||||||
|             name: "ntfy-webhook".to_string(), |  | ||||||
|             url: Url::Url( |  | ||||||
|                 url::Url::parse( |  | ||||||
|                     format!( |  | ||||||
|                         "http://ntfy.{}.svc.cluster.local/rust-web-app?auth={ntfy_default_auth_param}", |  | ||||||
|                         namespace.clone() |  | ||||||
|                     ) |  | ||||||
|                     .as_str(), |  | ||||||
|                 ) |  | ||||||
|                 .unwrap(), |  | ||||||
|             ), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         alerting_score.receivers.push(Box::new(ntfy_receiver)); |  | ||||||
|         alerting_score |  | ||||||
|             .interpret(&Inventory::empty(), topology) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| e.to_string())?; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
|     fn name(&self) -> String { |  | ||||||
|         "Monitoring".to_string() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,5 +1,4 @@ | |||||||
| use std::fs::{self, File}; | use std::fs; | ||||||
| use std::io::Read; |  | ||||||
| use std::path::{Path, PathBuf}; | use std::path::{Path, PathBuf}; | ||||||
| use std::process; | use std::process; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| @ -13,8 +12,7 @@ use dockerfile_builder::instruction_builder::CopyBuilder; | |||||||
| use futures_util::StreamExt; | use futures_util::StreamExt; | ||||||
| use log::{debug, info, log_enabled}; | use log::{debug, info, log_enabled}; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use tar::{Archive, Builder, Header}; | use tar::Archive; | ||||||
| use walkdir::WalkDir; |  | ||||||
| 
 | 
 | ||||||
| use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; | use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; | ||||||
| use crate::{score::Score, topology::Topology}; | use crate::{score::Score, topology::Topology}; | ||||||
| @ -61,7 +59,6 @@ pub struct RustWebapp { | |||||||
|     pub domain: Url, |     pub domain: Url, | ||||||
|     /// The path to the root of the Rust project to be containerized.
 |     /// The path to the root of the Rust project to be containerized.
 | ||||||
|     pub project_root: PathBuf, |     pub project_root: PathBuf, | ||||||
|     pub service_port: u32, |  | ||||||
|     pub framework: Option<RustWebFramework>, |     pub framework: Option<RustWebFramework>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -161,99 +158,45 @@ impl RustWebapp { | |||||||
|         image_name: &str, |         image_name: &str, | ||||||
|     ) -> Result<String, Box<dyn std::error::Error>> { |     ) -> Result<String, Box<dyn std::error::Error>> { | ||||||
|         debug!("Generating Dockerfile for '{}'", self.name); |         debug!("Generating Dockerfile for '{}'", self.name); | ||||||
|         let dockerfile = self.get_or_build_dockerfile(); |         let _dockerfile_path = self.build_dockerfile()?; | ||||||
|  | 
 | ||||||
|  |         let docker = Docker::connect_with_socket_defaults().unwrap(); | ||||||
|  | 
 | ||||||
|         let quiet = !log_enabled!(log::Level::Debug); |         let quiet = !log_enabled!(log::Level::Debug); | ||||||
|         match dockerfile | 
 | ||||||
|  |         let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default() | ||||||
|  |             .dockerfile("Dockerfile.harmony") | ||||||
|  |             .t(image_name) | ||||||
|  |             .q(quiet) | ||||||
|  |             .version(bollard::query_parameters::BuilderVersion::BuilderV1) | ||||||
|  |             .platform("linux/x86_64"); | ||||||
|  | 
 | ||||||
|  |         let mut temp_tar_builder = tar::Builder::new(Vec::new()); | ||||||
|  |         temp_tar_builder | ||||||
|  |             .append_dir_all("", self.project_root.clone()) | ||||||
|  |             .unwrap(); | ||||||
|  |         let archive = temp_tar_builder | ||||||
|  |             .into_inner() | ||||||
|  |             .expect("couldn't finish creating tar"); | ||||||
|  |         let archived_files = Archive::new(archive.as_slice()) | ||||||
|  |             .entries() | ||||||
|             .unwrap() |             .unwrap() | ||||||
|             .file_name() |             .map(|entry| entry.unwrap().path().unwrap().into_owned()) | ||||||
|             .and_then(|os_str| os_str.to_str()) |             .collect::<Vec<_>>(); | ||||||
|         { |  | ||||||
|             Some(path_str) => { |  | ||||||
|                 debug!("Building from dockerfile {}", path_str); |  | ||||||
| 
 | 
 | ||||||
|                 let tar_data = self |         debug!("files in docker tar: {:#?}", archived_files); | ||||||
|                     .create_deterministic_tar(&self.project_root.clone()) |  | ||||||
|                     .await |  | ||||||
|                     .unwrap(); |  | ||||||
| 
 | 
 | ||||||
|                 let docker = Docker::connect_with_socket_defaults().unwrap(); |         let mut image_build_stream = docker.build_image( | ||||||
|  |             build_image_options.build(), | ||||||
|  |             None, | ||||||
|  |             Some(body_full(archive.into())), | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|                 let build_image_options = |         while let Some(msg) = image_build_stream.next().await { | ||||||
|                     bollard::query_parameters::BuildImageOptionsBuilder::default() |             debug!("Message: {msg:?}"); | ||||||
|                         .dockerfile(path_str) |  | ||||||
|                         .t(image_name) |  | ||||||
|                         .q(quiet) |  | ||||||
|                         .version(bollard::query_parameters::BuilderVersion::BuilderV1) |  | ||||||
|                         .platform("linux/x86_64"); |  | ||||||
| 
 |  | ||||||
|                 let mut image_build_stream = docker.build_image( |  | ||||||
|                     build_image_options.build(), |  | ||||||
|                     None, |  | ||||||
|                     Some(body_full(tar_data.into())), |  | ||||||
|                 ); |  | ||||||
| 
 |  | ||||||
|                 while let Some(msg) = image_build_stream.next().await { |  | ||||||
|                     debug!("Message: {msg:?}"); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 Ok(image_name.to_string()) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             None => Err(Box::new(std::io::Error::new( |  | ||||||
|                 std::io::ErrorKind::InvalidData, |  | ||||||
|                 "Path is not valid UTF-8", |  | ||||||
|             ))), |  | ||||||
|         } |         } | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     ///normalizes timestamp and ignores files that will bust the docker cach
 |         Ok(image_name.to_string()) | ||||||
|     async fn create_deterministic_tar( |  | ||||||
|         &self, |  | ||||||
|         project_root: &std::path::Path, |  | ||||||
|     ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { |  | ||||||
|         debug!("building tar file from project root {:#?}", project_root); |  | ||||||
|         let mut tar_data = Vec::new(); |  | ||||||
|         { |  | ||||||
|             let mut builder = Builder::new(&mut tar_data); |  | ||||||
|             let ignore_prefixes = [ |  | ||||||
|                 "target", |  | ||||||
|                 ".git", |  | ||||||
|                 ".github", |  | ||||||
|                 ".harmony_generated", |  | ||||||
|                 "node_modules", |  | ||||||
|             ]; |  | ||||||
|             let mut entries: Vec<_> = WalkDir::new(project_root) |  | ||||||
|                 .into_iter() |  | ||||||
|                 .filter_map(Result::ok) |  | ||||||
|                 .filter(|e| e.file_type().is_file()) |  | ||||||
|                 .filter(|e| { |  | ||||||
|                     let rel_path = e.path().strip_prefix(project_root).unwrap(); |  | ||||||
|                     !ignore_prefixes |  | ||||||
|                         .iter() |  | ||||||
|                         .any(|prefix| rel_path.starts_with(prefix)) |  | ||||||
|                 }) |  | ||||||
|                 .collect(); |  | ||||||
|             entries.sort_by_key(|e| e.path().to_owned()); |  | ||||||
| 
 |  | ||||||
|             for entry in entries { |  | ||||||
|                 let path = entry.path(); |  | ||||||
|                 let rel_path = path.strip_prefix(project_root).unwrap(); |  | ||||||
| 
 |  | ||||||
|                 let mut file = fs::File::open(path)?; |  | ||||||
|                 let mut header = Header::new_gnu(); |  | ||||||
| 
 |  | ||||||
|                 header.set_size(entry.metadata()?.len()); |  | ||||||
|                 header.set_mode(0o644); |  | ||||||
|                 header.set_mtime(0); |  | ||||||
|                 header.set_uid(0); |  | ||||||
|                 header.set_gid(0); |  | ||||||
| 
 |  | ||||||
|                 builder.append_data(&mut header, rel_path, &mut file)?; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             builder.finish()?; |  | ||||||
|         } |  | ||||||
|         Ok(tar_data) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Tags and pushes a Docker image to the configured remote registry.
 |     /// Tags and pushes a Docker image to the configured remote registry.
 | ||||||
| @ -329,11 +272,8 @@ impl RustWebapp { | |||||||
|                     "groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser", |                     "groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser", | ||||||
|                 )); |                 )); | ||||||
| 
 | 
 | ||||||
|                 dockerfile.push(ENV::from(format!( |                 dockerfile.push(ENV::from("LEPTOS_SITE_ADDR=0.0.0.0:3000")); | ||||||
|                     "LEPTOS_SITE_ADDR=0.0.0.0:{}", |                 dockerfile.push(EXPOSE::from("3000/tcp")); | ||||||
|                     self.service_port |  | ||||||
|                 ))); |  | ||||||
|                 dockerfile.push(EXPOSE::from(format!("{}/tcp", self.service_port))); |  | ||||||
|                 dockerfile.push(WORKDIR::from("/home/appuser")); |                 dockerfile.push(WORKDIR::from("/home/appuser")); | ||||||
| 
 | 
 | ||||||
|                 // Copy static files
 |                 // Copy static files
 | ||||||
| @ -454,7 +394,7 @@ image: | |||||||
| 
 | 
 | ||||||
| service: | service: | ||||||
|   type: ClusterIP |   type: ClusterIP | ||||||
|   port: {} |   port: 3000 | ||||||
| 
 | 
 | ||||||
| ingress: | ingress: | ||||||
|   enabled: true |   enabled: true | ||||||
| @ -474,123 +414,112 @@ ingress: | |||||||
|        - chart-example.local |        - chart-example.local | ||||||
| 
 | 
 | ||||||
| "#,
 | "#,
 | ||||||
|             chart_name, image_repo, image_tag, self.service_port, self.name |             chart_name, image_repo, image_tag, self.name | ||||||
|         ); |         ); | ||||||
|         fs::write(chart_dir.join("values.yaml"), values_yaml)?; |         fs::write(chart_dir.join("values.yaml"), values_yaml)?; | ||||||
| 
 | 
 | ||||||
|         // Create templates/_helpers.tpl
 |         // Create templates/_helpers.tpl
 | ||||||
|         let helpers_tpl = format!( |         let helpers_tpl = r#" | ||||||
|             r#" | {{/* | ||||||
| {{{{/* |  | ||||||
| Expand the name of the chart. | Expand the name of the chart. | ||||||
| */}}}} | */}} | ||||||
| {{{{- define "chart.name" -}}}} | {{- define "chart.name" -}} | ||||||
| {{{{- default .Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }}}} | {{- default .Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }} | ||||||
| {{{{- end }}}} | {{- end }} | ||||||
| 
 | 
 | ||||||
| {{{{/* | {{/* | ||||||
| Create a default fully qualified app name. | Create a default fully qualified app name. | ||||||
| We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). | ||||||
| */}}}} | */}} | ||||||
| {{{{- define "chart.fullname" -}}}} | {{- define "chart.fullname" -}} | ||||||
| {{{{- $name := default .Chart.Name $.Values.nameOverride }}}} | {{- $name := default .Chart.Name $.Values.nameOverride }} | ||||||
| {{{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}}} | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} | ||||||
| {{{{- end }}}} | {{- end }} | ||||||
| "#
 | "#;
 | ||||||
|         ); |  | ||||||
|         fs::write(templates_dir.join("_helpers.tpl"), helpers_tpl)?; |         fs::write(templates_dir.join("_helpers.tpl"), helpers_tpl)?; | ||||||
| 
 | 
 | ||||||
|         // Create templates/service.yaml
 |         // Create templates/service.yaml
 | ||||||
|         let service_yaml = format!( |         let service_yaml = r#" | ||||||
|             r#" |  | ||||||
| apiVersion: v1 | apiVersion: v1 | ||||||
| kind: Service | kind: Service | ||||||
| metadata: | metadata: | ||||||
|   name: {{{{ include "chart.fullname" . }}}} |   name: {{ include "chart.fullname" . }} | ||||||
| spec: | spec: | ||||||
|   type: {{{{ $.Values.service.type }}}} |   type: {{ $.Values.service.type }} | ||||||
|   ports: |   ports: | ||||||
|     - name: main |     - name: main | ||||||
|       port: {{{{ $.Values.service.port | default {} }}}} |       port: {{ $.Values.service.port | default 3000 }} | ||||||
|       targetPort: {{{{ $.Values.service.port | default {} }}}} |       targetPort: {{ $.Values.service.port | default 3000 }} | ||||||
|       protocol: TCP |       protocol: TCP | ||||||
|   selector: |   selector: | ||||||
|     app: {{{{ include "chart.name" . }}}} |     app: {{ include "chart.name" . }} | ||||||
| "#,
 | "#;
 | ||||||
|             self.service_port, self.service_port |  | ||||||
|         ); |  | ||||||
|         fs::write(templates_dir.join("service.yaml"), service_yaml)?; |         fs::write(templates_dir.join("service.yaml"), service_yaml)?; | ||||||
| 
 | 
 | ||||||
|         // Create templates/deployment.yaml
 |         // Create templates/deployment.yaml
 | ||||||
|         let deployment_yaml = format!( |         let deployment_yaml = r#" | ||||||
|             r#" |  | ||||||
| apiVersion: apps/v1 | apiVersion: apps/v1 | ||||||
| kind: Deployment | kind: Deployment | ||||||
| metadata: | metadata: | ||||||
|   name: {{{{ include "chart.fullname" . }}}} |   name: {{ include "chart.fullname" . }} | ||||||
| spec: | spec: | ||||||
|   replicas: {{{{ $.Values.replicaCount }}}} |   replicas: {{ $.Values.replicaCount }} | ||||||
|   selector: |   selector: | ||||||
|     matchLabels: |     matchLabels: | ||||||
|       app: {{{{ include "chart.name" . }}}} |       app: {{ include "chart.name" . }} | ||||||
|   template: |   template: | ||||||
|     metadata: |     metadata: | ||||||
|       labels: |       labels: | ||||||
|         app: {{{{ include "chart.name" . }}}} |         app: {{ include "chart.name" . }} | ||||||
|     spec: |     spec: | ||||||
|       containers: |       containers: | ||||||
|         - name: {{{{ .Chart.Name }}}} |         - name: {{ .Chart.Name }} | ||||||
|           image: "{{{{ $.Values.image.repository }}}}:{{{{ $.Values.image.tag | default .Chart.AppVersion }}}}" |           image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag | default .Chart.AppVersion }}" | ||||||
|           imagePullPolicy: {{{{ $.Values.image.pullPolicy }}}} |           imagePullPolicy: {{ $.Values.image.pullPolicy }} | ||||||
|           ports: |           ports: | ||||||
|             - name: main |             - name: main | ||||||
|               containerPort: {{{{ $.Values.service.port | default {} }}}} |               containerPort: {{ $.Values.service.port | default 3000 }} | ||||||
|               protocol: TCP |               protocol: TCP | ||||||
| "#,
 | "#;
 | ||||||
|             self.service_port |  | ||||||
|         ); |  | ||||||
|         fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?; |         fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?; | ||||||
| 
 | 
 | ||||||
|         // Create templates/ingress.yaml
 |         // Create templates/ingress.yaml
 | ||||||
|         let ingress_yaml = format!( |         let ingress_yaml = r#" | ||||||
|             r#" | {{- if $.Values.ingress.enabled -}} | ||||||
| {{{{- if $.Values.ingress.enabled -}}}} |  | ||||||
| apiVersion: networking.k8s.io/v1 | apiVersion: networking.k8s.io/v1 | ||||||
| kind: Ingress | kind: Ingress | ||||||
| metadata: | metadata: | ||||||
|   name: {{{{ include "chart.fullname" . }}}} |   name: {{ include "chart.fullname" . }} | ||||||
|   annotations: |   annotations: | ||||||
|     {{{{- toYaml $.Values.ingress.annotations | nindent 4 }}}} |     {{- toYaml $.Values.ingress.annotations | nindent 4 }} | ||||||
| spec: | spec: | ||||||
|   {{{{- if $.Values.ingress.tls }}}} |   {{- if $.Values.ingress.tls }} | ||||||
|   tls: |   tls: | ||||||
|     {{{{- range $.Values.ingress.tls }}}} |     {{- range $.Values.ingress.tls }} | ||||||
|     - hosts: |     - hosts: | ||||||
|         {{{{- range .hosts }}}} |         {{- range .hosts }} | ||||||
|         - {{{{ . | quote }}}} |         - {{ . | quote }} | ||||||
|         {{{{- end }}}} |         {{- end }} | ||||||
|       secretName: {{{{ .secretName }}}} |       secretName: {{ .secretName }} | ||||||
|     {{{{- end }}}} |     {{- end }} | ||||||
|   {{{{- end }}}} |   {{- end }} | ||||||
|   rules: |   rules: | ||||||
|     {{{{- range $.Values.ingress.hosts }}}} |     {{- range $.Values.ingress.hosts }} | ||||||
|     - host: {{{{ .host | quote }}}} |     - host: {{ .host | quote }} | ||||||
|       http: |       http: | ||||||
|         paths: |         paths: | ||||||
|           {{{{- range .paths }}}} |           {{- range .paths }} | ||||||
|           - path: {{{{ .path }}}} |           - path: {{ .path }} | ||||||
|             pathType: {{{{ .pathType }}}} |             pathType: {{ .pathType }} | ||||||
|             backend: |             backend: | ||||||
|               service: |               service: | ||||||
|                 name: {{{{ include "chart.fullname" $ }}}} |                 name: {{ include "chart.fullname" $ }} | ||||||
|                 port: |                 port: | ||||||
|                   number: {{{{ $.Values.service.port | default {} }}}} |                   number: {{ $.Values.service.port | default 3000 }} | ||||||
|           {{{{- end }}}} |           {{- end }} | ||||||
|     {{{{- end }}}} |     {{- end }} | ||||||
| {{{{- end }}}} | {{- end }} | ||||||
| "#,
 | "#;
 | ||||||
|             self.service_port |  | ||||||
|         ); |  | ||||||
|         fs::write(templates_dir.join("ingress.yaml"), ingress_yaml)?; |         fs::write(templates_dir.join("ingress.yaml"), ingress_yaml)?; | ||||||
| 
 | 
 | ||||||
|         Ok(chart_dir) |         Ok(chart_dir) | ||||||
| @ -642,6 +571,7 @@ spec: | |||||||
|         let chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap(); |         let chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap(); | ||||||
|         let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT); |         let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT); | ||||||
|         let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name); |         let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name); | ||||||
|  | 
 | ||||||
|         debug!( |         debug!( | ||||||
|             "Pushing Helm chart {} to {}", |             "Pushing Helm chart {} to {}", | ||||||
|             packaged_chart_path.to_string_lossy(), |             packaged_chart_path.to_string_lossy(), | ||||||
| @ -660,20 +590,4 @@ spec: | |||||||
|         debug!("push url {oci_push_url}"); |         debug!("push url {oci_push_url}"); | ||||||
|         Ok(format!("{}:{}", oci_pull_url, version)) |         Ok(format!("{}:{}", oci_pull_url, version)) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     fn get_or_build_dockerfile(&self) -> Result<PathBuf, Box<dyn std::error::Error>> { |  | ||||||
|         let existing_dockerfile = self.project_root.join("Dockerfile"); |  | ||||||
| 
 |  | ||||||
|         debug!("project_root = {:?}", self.project_root); |  | ||||||
| 
 |  | ||||||
|         debug!("checking = {:?}", existing_dockerfile); |  | ||||||
|         if existing_dockerfile.exists() { |  | ||||||
|             debug!( |  | ||||||
|                 "Checking path {:#?} for existing Dockerfile", |  | ||||||
|                 self.project_root.clone() |  | ||||||
|             ); |  | ||||||
|             return Ok(existing_dockerfile); |  | ||||||
|         } |  | ||||||
|         self.build_dockerfile() |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ use std::collections::BTreeMap; | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use k8s_openapi::api::core::v1::Secret; | use k8s_openapi::api::core::v1::Secret; | ||||||
| use kube::api::ObjectMeta; | use kube::api::ObjectMeta; | ||||||
| use log::debug; |  | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use serde_json::json; | use serde_json::json; | ||||||
| use serde_yaml::{Mapping, Value}; | use serde_yaml::{Mapping, Value}; | ||||||
| @ -12,7 +11,6 @@ use serde_yaml::{Mapping, Value}; | |||||||
| use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::{ | use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::{ | ||||||
|     AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus, |     AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus, | ||||||
| }; | }; | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability; |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     interpret::{InterpretError, Outcome}, |     interpret::{InterpretError, Outcome}, | ||||||
|     modules::monitoring::{ |     modules::monitoring::{ | ||||||
| @ -32,71 +30,6 @@ pub struct DiscordWebhook { | |||||||
|     pub url: Url, |     pub url: Url, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] |  | ||||||
| impl AlertReceiver<RHOBObservability> for DiscordWebhook { |  | ||||||
|     async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> { |  | ||||||
|         let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec { |  | ||||||
|             data: json!({ |  | ||||||
|                 "route": { |  | ||||||
|                     "receiver": self.name, |  | ||||||
|                 }, |  | ||||||
|                 "receivers": [ |  | ||||||
|                     { |  | ||||||
|                         "name": self.name, |  | ||||||
|                         "webhookConfigs": [ |  | ||||||
|                             { |  | ||||||
|                             "url": self.url, |  | ||||||
|                             } |  | ||||||
|                         ] |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             }), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         let alertmanager_configs = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfig { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(self.name.clone()), |  | ||||||
|                 labels: Some(std::collections::BTreeMap::from([( |  | ||||||
|                     "alertmanagerConfig".to_string(), |  | ||||||
|                     "enabled".to_string(), |  | ||||||
|                 )])), |  | ||||||
|                 namespace: Some(sender.namespace.clone()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec, |  | ||||||
|         }; |  | ||||||
|         debug!( |  | ||||||
|             "alertmanager_configs yaml:\n{:#?}", |  | ||||||
|             serde_yaml::to_string(&alertmanager_configs) |  | ||||||
|         ); |  | ||||||
|         debug!( |  | ||||||
|             "alert manager configs: \n{:#?}", |  | ||||||
|             alertmanager_configs.clone() |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         sender |  | ||||||
|             .client |  | ||||||
|             .apply(&alertmanager_configs, Some(&sender.namespace)) |  | ||||||
|             .await?; |  | ||||||
|         Ok(Outcome::success(format!( |  | ||||||
|             "installed rhob-alertmanagerconfigs for {}", |  | ||||||
|             self.name |  | ||||||
|         ))) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn name(&self) -> String { |  | ||||||
|         "webhook-receiver".to_string() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn clone_box(&self) -> Box<dyn AlertReceiver<RHOBObservability>> { |  | ||||||
|         Box::new(self.clone()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn as_any(&self) -> &dyn Any { |  | ||||||
|         self |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl AlertReceiver<CRDPrometheus> for DiscordWebhook { | impl AlertReceiver<CRDPrometheus> for DiscordWebhook { | ||||||
|     async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> { |     async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> { | ||||||
|  | |||||||
| @ -11,8 +11,8 @@ use crate::{ | |||||||
|     interpret::{InterpretError, Outcome}, |     interpret::{InterpretError, Outcome}, | ||||||
|     modules::monitoring::{ |     modules::monitoring::{ | ||||||
|         kube_prometheus::{ |         kube_prometheus::{ | ||||||
|             crd::{ |             crd::crd_alertmanager_config::{ | ||||||
|                 crd_alertmanager_config::CRDPrometheus, rhob_alertmanager_config::RHOBObservability, |                 AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus, | ||||||
|             }, |             }, | ||||||
|             prometheus::{KubePrometheus, KubePrometheusReceiver}, |             prometheus::{KubePrometheus, KubePrometheusReceiver}, | ||||||
|             types::{AlertChannelConfig, AlertManagerChannelConfig}, |             types::{AlertChannelConfig, AlertManagerChannelConfig}, | ||||||
| @ -29,71 +29,10 @@ pub struct WebhookReceiver { | |||||||
|     pub url: Url, |     pub url: Url, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] |  | ||||||
| impl AlertReceiver<RHOBObservability> for WebhookReceiver { |  | ||||||
|     async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> { |  | ||||||
|         let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec { |  | ||||||
|             data: json!({ |  | ||||||
|                 "route": { |  | ||||||
|                     "receiver": self.name, |  | ||||||
|                 }, |  | ||||||
|                 "receivers": [ |  | ||||||
|                     { |  | ||||||
|                         "name": self.name, |  | ||||||
|                         "webhookConfigs": [ |  | ||||||
|                             { |  | ||||||
|                             "url": self.url, |  | ||||||
|                             } |  | ||||||
|                         ] |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             }), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         let alertmanager_configs = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfig { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(self.name.clone()), |  | ||||||
|                 labels: Some(std::collections::BTreeMap::from([( |  | ||||||
|                     "alertmanagerConfig".to_string(), |  | ||||||
|                     "enabled".to_string(), |  | ||||||
|                 )])), |  | ||||||
|                 namespace: Some(sender.namespace.clone()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec, |  | ||||||
|         }; |  | ||||||
|         debug!( |  | ||||||
|             "alert manager configs: \n{:#?}", |  | ||||||
|             alertmanager_configs.clone() |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         sender |  | ||||||
|             .client |  | ||||||
|             .apply(&alertmanager_configs, Some(&sender.namespace)) |  | ||||||
|             .await?; |  | ||||||
|         Ok(Outcome::success(format!( |  | ||||||
|             "installed rhob-alertmanagerconfigs for {}", |  | ||||||
|             self.name |  | ||||||
|         ))) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn name(&self) -> String { |  | ||||||
|         "webhook-receiver".to_string() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn clone_box(&self) -> Box<dyn AlertReceiver<RHOBObservability>> { |  | ||||||
|         Box::new(self.clone()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn as_any(&self) -> &dyn Any { |  | ||||||
|         self |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl AlertReceiver<CRDPrometheus> for WebhookReceiver { | impl AlertReceiver<CRDPrometheus> for WebhookReceiver { | ||||||
|     async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> { |     async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> { | ||||||
|         let spec = crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::AlertmanagerConfigSpec { |         let spec = AlertmanagerConfigSpec { | ||||||
|             data: json!({ |             data: json!({ | ||||||
|                 "route": { |                 "route": { | ||||||
|                     "receiver": self.name, |                     "receiver": self.name, | ||||||
| @ -111,7 +50,7 @@ impl AlertReceiver<CRDPrometheus> for WebhookReceiver { | |||||||
|             }), |             }), | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let alertmanager_configs = crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::AlertmanagerConfig { |         let alertmanager_configs = AlertmanagerConfig { | ||||||
|             metadata: ObjectMeta { |             metadata: ObjectMeta { | ||||||
|                 name: Some(self.name.clone()), |                 name: Some(self.name.clone()), | ||||||
|                 labels: Some(std::collections::BTreeMap::from([( |                 labels: Some(std::collections::BTreeMap::from([( | ||||||
| @ -176,7 +115,6 @@ impl PrometheusReceiver for WebhookReceiver { | |||||||
|         self.get_config().await |         self.get_config().await | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl AlertReceiver<KubePrometheus> for WebhookReceiver { | impl AlertReceiver<KubePrometheus> for WebhookReceiver { | ||||||
|     async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> { |     async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> { | ||||||
|  | |||||||
| @ -1,2 +1 @@ | |||||||
| pub mod application_monitoring_score; | pub mod application_monitoring_score; | ||||||
| pub mod rhobs_application_monitoring_score; |  | ||||||
|  | |||||||
| @ -1,94 +0,0 @@ | |||||||
| use std::sync::Arc; |  | ||||||
| 
 |  | ||||||
| use async_trait::async_trait; |  | ||||||
| use serde::Serialize; |  | ||||||
| 
 |  | ||||||
| use crate::{ |  | ||||||
|     data::Version, |  | ||||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, |  | ||||||
|     inventory::Inventory, |  | ||||||
|     modules::{ |  | ||||||
|         application::Application, |  | ||||||
|         monitoring::kube_prometheus::crd::{ |  | ||||||
|             crd_alertmanager_config::CRDPrometheus, rhob_alertmanager_config::RHOBObservability, |  | ||||||
|         }, |  | ||||||
|         prometheus::prometheus::PrometheusApplicationMonitoring, |  | ||||||
|     }, |  | ||||||
|     score::Score, |  | ||||||
|     topology::{PreparationOutcome, Topology, oberservability::monitoring::AlertReceiver}, |  | ||||||
| }; |  | ||||||
| use harmony_types::id::Id; |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Serialize)] |  | ||||||
| pub struct ApplicationRHOBMonitoringScore { |  | ||||||
|     pub sender: RHOBObservability, |  | ||||||
|     pub application: Arc<dyn Application>, |  | ||||||
|     pub receivers: Vec<Box<dyn AlertReceiver<RHOBObservability>>>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl<T: Topology + PrometheusApplicationMonitoring<RHOBObservability>> Score<T> |  | ||||||
|     for ApplicationRHOBMonitoringScore |  | ||||||
| { |  | ||||||
|     fn create_interpret(&self) -> Box<dyn Interpret<T>> { |  | ||||||
|         Box::new(ApplicationRHOBMonitoringInterpret { |  | ||||||
|             score: self.clone(), |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn name(&self) -> String { |  | ||||||
|         format!( |  | ||||||
|             "{} monitoring [ApplicationRHOBMonitoringScore]", |  | ||||||
|             self.application.name() |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub struct ApplicationRHOBMonitoringInterpret { |  | ||||||
|     score: ApplicationRHOBMonitoringScore, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[async_trait] |  | ||||||
| impl<T: Topology + PrometheusApplicationMonitoring<RHOBObservability>> Interpret<T> |  | ||||||
|     for ApplicationRHOBMonitoringInterpret |  | ||||||
| { |  | ||||||
|     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()), |  | ||||||
|             }, |  | ||||||
|             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!() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -7,15 +7,5 @@ pub mod crd_prometheuses; | |||||||
| pub mod grafana_default_dashboard; | pub mod grafana_default_dashboard; | ||||||
| pub mod grafana_operator; | pub mod grafana_operator; | ||||||
| pub mod prometheus_operator; | pub mod prometheus_operator; | ||||||
| pub mod rhob_alertmanager_config; |  | ||||||
| pub mod rhob_alertmanagers; |  | ||||||
| pub mod rhob_cluster_observability_operator; |  | ||||||
| pub mod rhob_default_rules; |  | ||||||
| pub mod rhob_grafana; |  | ||||||
| pub mod rhob_monitoring_stack; |  | ||||||
| pub mod rhob_prometheus_rules; |  | ||||||
| pub mod rhob_prometheuses; |  | ||||||
| pub mod rhob_role; |  | ||||||
| pub mod rhob_service_monitor; |  | ||||||
| pub mod role; | pub mod role; | ||||||
| pub mod service_monitor; | pub mod service_monitor; | ||||||
|  | |||||||
| @ -1,50 +0,0 @@ | |||||||
| use std::sync::Arc; |  | ||||||
| 
 |  | ||||||
| use kube::CustomResource; |  | ||||||
| use schemars::JsonSchema; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| 
 |  | ||||||
| use crate::topology::{ |  | ||||||
|     k8s::K8sClient, |  | ||||||
|     oberservability::monitoring::{AlertReceiver, AlertSender}, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "monitoring.rhobs", |  | ||||||
|     version = "v1alpha1", |  | ||||||
|     kind = "AlertmanagerConfig", |  | ||||||
|     plural = "alertmanagerconfigs", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| pub struct AlertmanagerConfigSpec { |  | ||||||
|     #[serde(flatten)] |  | ||||||
|     pub data: serde_json::Value, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Serialize)] |  | ||||||
| pub struct RHOBObservability { |  | ||||||
|     pub namespace: String, |  | ||||||
|     pub client: Arc<K8sClient>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl AlertSender for RHOBObservability { |  | ||||||
|     fn name(&self) -> String { |  | ||||||
|         "RHOBAlertManager".to_string() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Clone for Box<dyn AlertReceiver<RHOBObservability>> { |  | ||||||
|     fn clone(&self) -> Self { |  | ||||||
|         self.clone_box() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Serialize for Box<dyn AlertReceiver<RHOBObservability>> { |  | ||||||
|     fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> |  | ||||||
|     where |  | ||||||
|         S: serde::Serializer, |  | ||||||
|     { |  | ||||||
|         todo!() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,52 +0,0 @@ | |||||||
| use kube::CustomResource; |  | ||||||
| use schemars::JsonSchema; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| 
 |  | ||||||
| use super::crd_prometheuses::LabelSelector; |  | ||||||
| 
 |  | ||||||
| /// Rust CRD for `Alertmanager` from Prometheus Operator
 |  | ||||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "monitoring.rhobs", |  | ||||||
|     version = "v1", |  | ||||||
|     kind = "Alertmanager", |  | ||||||
|     plural = "alertmanagers", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct AlertmanagerSpec { |  | ||||||
|     /// Number of replicas for HA
 |  | ||||||
|     pub replicas: i32, |  | ||||||
| 
 |  | ||||||
|     /// Selectors for AlertmanagerConfig CRDs
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub alertmanager_config_selector: Option<LabelSelector>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub alertmanager_config_namespace_selector: Option<LabelSelector>, |  | ||||||
| 
 |  | ||||||
|     /// Optional pod template metadata (annotations, labels)
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub pod_metadata: Option<LabelSelector>, |  | ||||||
| 
 |  | ||||||
|     /// Optional topology spread settings
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub version: Option<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for AlertmanagerSpec { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         AlertmanagerSpec { |  | ||||||
|             replicas: 1, |  | ||||||
| 
 |  | ||||||
|             // Match all AlertmanagerConfigs in the same namespace
 |  | ||||||
|             alertmanager_config_namespace_selector: None, |  | ||||||
| 
 |  | ||||||
|             // Empty selector matches all AlertmanagerConfigs in that namespace
 |  | ||||||
|             alertmanager_config_selector: Some(LabelSelector::default()), |  | ||||||
| 
 |  | ||||||
|             pod_metadata: None, |  | ||||||
|             version: None, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,22 +0,0 @@ | |||||||
| use std::str::FromStr; |  | ||||||
| 
 |  | ||||||
| use non_blank_string_rs::NonBlankString; |  | ||||||
| 
 |  | ||||||
| use crate::modules::helm::chart::HelmChartScore; |  | ||||||
| //TODO package chart or something for COO okd
 |  | ||||||
| pub fn rhob_cluster_observability_operator() -> HelmChartScore { |  | ||||||
|     HelmChartScore { |  | ||||||
|         namespace: None, |  | ||||||
|         release_name: NonBlankString::from_str("").unwrap(), |  | ||||||
|         chart_name: NonBlankString::from_str( |  | ||||||
|             "oci://hub.nationtech.io/harmony/nt-prometheus-operator", |  | ||||||
|         ) |  | ||||||
|         .unwrap(), |  | ||||||
|         chart_version: None, |  | ||||||
|         values_overrides: None, |  | ||||||
|         values_yaml: None, |  | ||||||
|         create_namespace: true, |  | ||||||
|         install_only: true, |  | ||||||
|         repository: None, |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,26 +0,0 @@ | |||||||
| use crate::modules::{ |  | ||||||
|     monitoring::kube_prometheus::crd::rhob_prometheus_rules::Rule, |  | ||||||
|     prometheus::alerts::k8s::{ |  | ||||||
|         deployment::alert_deployment_unavailable, |  | ||||||
|         pod::{alert_container_restarting, alert_pod_not_ready, pod_failed}, |  | ||||||
|         pvc::high_pvc_fill_rate_over_two_days, |  | ||||||
|         service::alert_service_down, |  | ||||||
|     }, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| pub fn build_default_application_rules() -> Vec<Rule> { |  | ||||||
|     let pod_failed: Rule = pod_failed().into(); |  | ||||||
|     let container_restarting: Rule = alert_container_restarting().into(); |  | ||||||
|     let pod_not_ready: Rule = alert_pod_not_ready().into(); |  | ||||||
|     let service_down: Rule = alert_service_down().into(); |  | ||||||
|     let deployment_unavailable: Rule = alert_deployment_unavailable().into(); |  | ||||||
|     let high_pvc_fill_rate: Rule = high_pvc_fill_rate_over_two_days().into(); |  | ||||||
|     vec![ |  | ||||||
|         pod_failed, |  | ||||||
|         container_restarting, |  | ||||||
|         pod_not_ready, |  | ||||||
|         service_down, |  | ||||||
|         deployment_unavailable, |  | ||||||
|         high_pvc_fill_rate, |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,153 +0,0 @@ | |||||||
| use std::collections::BTreeMap; |  | ||||||
| 
 |  | ||||||
| use kube::CustomResource; |  | ||||||
| use schemars::JsonSchema; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| 
 |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheuses::LabelSelector; |  | ||||||
| 
 |  | ||||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "grafana.integreatly.org", |  | ||||||
|     version = "v1beta1", |  | ||||||
|     kind = "Grafana", |  | ||||||
|     plural = "grafanas", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaSpec { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub config: Option<GrafanaConfig>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub admin_user: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub admin_password: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub ingress: Option<GrafanaIngress>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub persistence: Option<GrafanaPersistence>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub resources: Option<ResourceRequirements>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaConfig { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub log: Option<GrafanaLogConfig>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub security: Option<GrafanaSecurityConfig>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaLogConfig { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub mode: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub level: Option<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaSecurityConfig { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub admin_user: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub admin_password: Option<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaIngress { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub enabled: Option<bool>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub hosts: Option<Vec<String>>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaPersistence { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub enabled: Option<bool>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub storage_class_name: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub size: Option<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ------------------------------------------------------------------------------------------------
 |  | ||||||
| 
 |  | ||||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "grafana.integreatly.org", |  | ||||||
|     version = "v1beta1", |  | ||||||
|     kind = "GrafanaDashboard", |  | ||||||
|     plural = "grafanadashboards", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaDashboardSpec { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub resync_period: Option<String>, |  | ||||||
| 
 |  | ||||||
|     pub instance_selector: LabelSelector, |  | ||||||
| 
 |  | ||||||
|     pub json: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ------------------------------------------------------------------------------------------------
 |  | ||||||
| 
 |  | ||||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "grafana.integreatly.org", |  | ||||||
|     version = "v1beta1", |  | ||||||
|     kind = "GrafanaDatasource", |  | ||||||
|     plural = "grafanadatasources", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaDatasourceSpec { |  | ||||||
|     pub instance_selector: LabelSelector, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub allow_cross_namespace_import: Option<bool>, |  | ||||||
| 
 |  | ||||||
|     pub datasource: GrafanaDatasourceConfig, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct GrafanaDatasourceConfig { |  | ||||||
|     pub access: String, |  | ||||||
|     pub database: Option<String>, |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub json_data: Option<BTreeMap<String, String>>, |  | ||||||
|     pub name: String, |  | ||||||
|     pub r#type: String, |  | ||||||
|     pub url: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ------------------------------------------------------------------------------------------------
 |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct ResourceRequirements { |  | ||||||
|     #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] |  | ||||||
|     pub limits: BTreeMap<String, String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] |  | ||||||
|     pub requests: BTreeMap<String, String>, |  | ||||||
| } |  | ||||||
| @ -1,41 +0,0 @@ | |||||||
| use std::collections::BTreeMap; |  | ||||||
| 
 |  | ||||||
| use kube::CustomResource; |  | ||||||
| use schemars::JsonSchema; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| 
 |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheuses::LabelSelector; |  | ||||||
| 
 |  | ||||||
| /// MonitoringStack CRD for monitoring.rhobs/v1alpha1
 |  | ||||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "monitoring.rhobs", |  | ||||||
|     version = "v1alpha1", |  | ||||||
|     kind = "MonitoringStack", |  | ||||||
|     plural = "monitoringstacks", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct MonitoringStackSpec { |  | ||||||
|     /// Verbosity of logs (e.g. "debug", "info", "warn", "error").
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub log_level: Option<String>, |  | ||||||
| 
 |  | ||||||
|     /// Retention period for Prometheus TSDB data (e.g. "1d").
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub retention: Option<String>, |  | ||||||
| 
 |  | ||||||
|     /// Resource selector for workloads monitored by this stack.
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub resource_selector: Option<LabelSelector>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for MonitoringStackSpec { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         MonitoringStackSpec { |  | ||||||
|             log_level: Some("info".into()), |  | ||||||
|             retention: Some("7d".into()), |  | ||||||
|             resource_selector: Some(LabelSelector::default()), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,57 +0,0 @@ | |||||||
| use std::collections::BTreeMap; |  | ||||||
| 
 |  | ||||||
| use kube::CustomResource; |  | ||||||
| use schemars::JsonSchema; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| 
 |  | ||||||
| use crate::modules::monitoring::alert_rule::prometheus_alert_rule::PrometheusAlertRule; |  | ||||||
| 
 |  | ||||||
| #[derive(CustomResource, Debug, Serialize, Deserialize, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "monitoring.rhobs", |  | ||||||
|     version = "v1", |  | ||||||
|     kind = "PrometheusRule", |  | ||||||
|     plural = "prometheusrules", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct PrometheusRuleSpec { |  | ||||||
|     pub groups: Vec<RuleGroup>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] |  | ||||||
| pub struct RuleGroup { |  | ||||||
|     pub name: String, |  | ||||||
|     pub rules: Vec<Rule>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct Rule { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub alert: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub expr: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub for_: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub labels: Option<std::collections::BTreeMap<String, String>>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub annotations: Option<std::collections::BTreeMap<String, String>>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl From<PrometheusAlertRule> for Rule { |  | ||||||
|     fn from(value: PrometheusAlertRule) -> Self { |  | ||||||
|         Rule { |  | ||||||
|             alert: Some(value.alert), |  | ||||||
|             expr: Some(value.expr), |  | ||||||
|             for_: value.r#for, |  | ||||||
|             labels: Some(value.labels.into_iter().collect::<BTreeMap<_, _>>()), |  | ||||||
|             annotations: Some(value.annotations.into_iter().collect::<BTreeMap<_, _>>()), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,118 +0,0 @@ | |||||||
| use std::collections::BTreeMap; |  | ||||||
| 
 |  | ||||||
| use kube::CustomResource; |  | ||||||
| use schemars::JsonSchema; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| 
 |  | ||||||
| use crate::modules::monitoring::kube_prometheus::types::Operator; |  | ||||||
| 
 |  | ||||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "monitoring.rhobs", |  | ||||||
|     version = "v1", |  | ||||||
|     kind = "Prometheus", |  | ||||||
|     plural = "prometheuses", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct PrometheusSpec { |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub alerting: Option<PrometheusSpecAlerting>, |  | ||||||
| 
 |  | ||||||
|     pub service_account_name: String, |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub service_monitor_namespace_selector: Option<LabelSelector>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub service_monitor_selector: Option<LabelSelector>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub service_discovery_role: Option<String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub pod_monitor_selector: Option<LabelSelector>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub rule_selector: Option<LabelSelector>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub rule_namespace_selector: Option<LabelSelector>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct NamespaceSelector { |  | ||||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] |  | ||||||
|     pub match_names: Vec<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Contains alerting configuration, specifically Alertmanager endpoints.
 |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] |  | ||||||
| pub struct PrometheusSpecAlerting { |  | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub alertmanagers: Option<Vec<AlertmanagerEndpoints>>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Represents an Alertmanager endpoint configuration used by Prometheus.
 |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] |  | ||||||
| pub struct AlertmanagerEndpoints { |  | ||||||
|     /// Name of the Alertmanager Service.
 |  | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub name: Option<String>, |  | ||||||
| 
 |  | ||||||
|     /// Namespace of the Alertmanager Service.
 |  | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub namespace: Option<String>, |  | ||||||
| 
 |  | ||||||
|     /// Port to access on the Alertmanager Service (e.g. "web").
 |  | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub port: Option<String>, |  | ||||||
| 
 |  | ||||||
|     /// Scheme to use for connecting (e.g. "http").
 |  | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub scheme: Option<String>, |  | ||||||
|     // Other fields like `tls_config`, `path_prefix`, etc., can be added if needed.
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct LabelSelector { |  | ||||||
|     #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] |  | ||||||
|     pub match_labels: BTreeMap<String, String>, |  | ||||||
| 
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] |  | ||||||
|     pub match_expressions: Vec<LabelSelectorRequirement>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct LabelSelectorRequirement { |  | ||||||
|     pub key: String, |  | ||||||
|     pub operator: Operator, |  | ||||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] |  | ||||||
|     pub values: Vec<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for PrometheusSpec { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         PrometheusSpec { |  | ||||||
|             alerting: None, |  | ||||||
| 
 |  | ||||||
|             service_account_name: "prometheus".into(), |  | ||||||
| 
 |  | ||||||
|             // null means "only my namespace"
 |  | ||||||
|             service_monitor_namespace_selector: None, |  | ||||||
| 
 |  | ||||||
|             // empty selector means match all ServiceMonitors in that namespace
 |  | ||||||
|             service_monitor_selector: Some(LabelSelector::default()), |  | ||||||
| 
 |  | ||||||
|             service_discovery_role: Some("Endpoints".into()), |  | ||||||
| 
 |  | ||||||
|             pod_monitor_selector: None, |  | ||||||
| 
 |  | ||||||
|             rule_selector: None, |  | ||||||
| 
 |  | ||||||
|             rule_namespace_selector: Some(LabelSelector::default()), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,62 +0,0 @@ | |||||||
| use k8s_openapi::api::{ |  | ||||||
|     core::v1::ServiceAccount, |  | ||||||
|     rbac::v1::{PolicyRule, Role, RoleBinding, RoleRef, Subject}, |  | ||||||
| }; |  | ||||||
| use kube::api::ObjectMeta; |  | ||||||
| 
 |  | ||||||
| pub fn build_prom_role(role_name: String, namespace: String) -> Role { |  | ||||||
|     Role { |  | ||||||
|         metadata: ObjectMeta { |  | ||||||
|             name: Some(role_name), |  | ||||||
|             namespace: Some(namespace), |  | ||||||
|             ..Default::default() |  | ||||||
|         }, |  | ||||||
|         rules: Some(vec![PolicyRule { |  | ||||||
|             api_groups: Some(vec!["".into()]), // core API group
 |  | ||||||
|             resources: Some(vec!["services".into(), "endpoints".into(), "pods".into()]), |  | ||||||
|             verbs: vec!["get".into(), "list".into(), "watch".into()], |  | ||||||
|             ..Default::default() |  | ||||||
|         }]), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub fn build_prom_rolebinding( |  | ||||||
|     role_name: String, |  | ||||||
|     namespace: String, |  | ||||||
|     service_account_name: String, |  | ||||||
| ) -> RoleBinding { |  | ||||||
|     RoleBinding { |  | ||||||
|         metadata: ObjectMeta { |  | ||||||
|             name: Some(format!("{}-rolebinding", role_name)), |  | ||||||
|             namespace: Some(namespace.clone()), |  | ||||||
|             ..Default::default() |  | ||||||
|         }, |  | ||||||
|         role_ref: RoleRef { |  | ||||||
|             api_group: "rbac.authorization.k8s.io".into(), |  | ||||||
|             kind: "Role".into(), |  | ||||||
|             name: role_name, |  | ||||||
|         }, |  | ||||||
|         subjects: Some(vec![Subject { |  | ||||||
|             kind: "ServiceAccount".into(), |  | ||||||
|             name: service_account_name, |  | ||||||
|             namespace: Some(namespace.clone()), |  | ||||||
|             ..Default::default() |  | ||||||
|         }]), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub fn build_prom_service_account( |  | ||||||
|     service_account_name: String, |  | ||||||
|     namespace: String, |  | ||||||
| ) -> ServiceAccount { |  | ||||||
|     ServiceAccount { |  | ||||||
|         automount_service_account_token: None, |  | ||||||
|         image_pull_secrets: None, |  | ||||||
|         metadata: ObjectMeta { |  | ||||||
|             name: Some(service_account_name), |  | ||||||
|             namespace: Some(namespace), |  | ||||||
|             ..Default::default() |  | ||||||
|         }, |  | ||||||
|         secrets: None, |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,87 +0,0 @@ | |||||||
| use std::collections::HashMap; |  | ||||||
| 
 |  | ||||||
| use kube::CustomResource; |  | ||||||
| use schemars::JsonSchema; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| 
 |  | ||||||
| use crate::modules::monitoring::kube_prometheus::types::{ |  | ||||||
|     HTTPScheme, MatchExpression, NamespaceSelector, Operator, Selector, |  | ||||||
|     ServiceMonitor as KubeServiceMonitor, ServiceMonitorEndpoint, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| /// This is the top-level struct for the ServiceMonitor Custom Resource.
 |  | ||||||
| /// The `#[derive(CustomResource)]` macro handles all the boilerplate for you,
 |  | ||||||
| /// including the `impl Resource`.
 |  | ||||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] |  | ||||||
| #[kube(
 |  | ||||||
|     group = "monitoring.rhobs", |  | ||||||
|     version = "v1", |  | ||||||
|     kind = "ServiceMonitor", |  | ||||||
|     plural = "servicemonitors", |  | ||||||
|     namespaced |  | ||||||
| )] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| pub struct ServiceMonitorSpec { |  | ||||||
|     /// A label selector to select services to monitor.
 |  | ||||||
|     pub selector: Selector, |  | ||||||
| 
 |  | ||||||
|     /// A list of endpoints on the selected services to be monitored.
 |  | ||||||
|     pub endpoints: Vec<ServiceMonitorEndpoint>, |  | ||||||
| 
 |  | ||||||
|     /// Selector to select which namespaces the Kubernetes Endpoints objects
 |  | ||||||
|     /// are discovered from.
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub namespace_selector: Option<NamespaceSelector>, |  | ||||||
| 
 |  | ||||||
|     /// The label to use to retrieve the job name from.
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |  | ||||||
|     pub job_label: Option<String>, |  | ||||||
| 
 |  | ||||||
|     /// Pod-based target labels to transfer from the Kubernetes Pod onto the target.
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] |  | ||||||
|     pub pod_target_labels: Vec<String>, |  | ||||||
| 
 |  | ||||||
|     /// TargetLabels transfers labels on the Kubernetes Service object to the target.
 |  | ||||||
|     #[serde(default, skip_serializing_if = "Vec::is_empty")] |  | ||||||
|     pub target_labels: Vec<String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for ServiceMonitorSpec { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         let labels = HashMap::new(); |  | ||||||
|         Self { |  | ||||||
|             selector: Selector { |  | ||||||
|                 match_labels: { labels }, |  | ||||||
|                 match_expressions: vec![MatchExpression { |  | ||||||
|                     key: "app.kubernetes.io/name".into(), |  | ||||||
|                     operator: Operator::Exists, |  | ||||||
|                     values: vec![], |  | ||||||
|                 }], |  | ||||||
|             }, |  | ||||||
|             endpoints: vec![ServiceMonitorEndpoint { |  | ||||||
|                 port: Some("http".to_string()), |  | ||||||
|                 path: Some("/metrics".into()), |  | ||||||
|                 interval: Some("30s".into()), |  | ||||||
|                 scheme: Some(HTTPScheme::HTTP), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }], |  | ||||||
|             namespace_selector: None, // only the same namespace
 |  | ||||||
|             job_label: Some("app".into()), |  | ||||||
|             pod_target_labels: vec![], |  | ||||||
|             target_labels: vec![], |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl From<KubeServiceMonitor> for ServiceMonitorSpec { |  | ||||||
|     fn from(value: KubeServiceMonitor) -> Self { |  | ||||||
|         Self { |  | ||||||
|             selector: value.selector, |  | ||||||
|             endpoints: value.endpoints, |  | ||||||
|             namespace_selector: value.namespace_selector, |  | ||||||
|             job_label: value.job_label, |  | ||||||
|             pod_target_labels: value.pod_target_labels, |  | ||||||
|             target_labels: value.target_labels, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -197,6 +197,11 @@ impl K8sPrometheusCRDAlertingInterpret { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn ensure_grafana_operator(&self) -> Result<Outcome, InterpretError> { |     async fn ensure_grafana_operator(&self) -> Result<Outcome, InterpretError> { | ||||||
|  |         if self.crd_exists("grafanas.grafana.integreatly.org").await { | ||||||
|  |             debug!("grafana CRDs already exist — skipping install."); | ||||||
|  |             return Ok(Outcome::success("Grafana CRDs already exist".to_string())); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         let _ = Command::new("helm") |         let _ = Command::new("helm") | ||||||
|             .args([ |             .args([ | ||||||
|                 "repo", |                 "repo", | ||||||
|  | |||||||
| @ -2,4 +2,3 @@ pub mod alerts; | |||||||
| pub mod k8s_prometheus_alerting_score; | pub mod k8s_prometheus_alerting_score; | ||||||
| #[allow(clippy::module_inception)] | #[allow(clippy::module_inception)] | ||||||
| pub mod prometheus; | pub mod prometheus; | ||||||
| pub mod rhob_alerting_score; |  | ||||||
|  | |||||||
| @ -1,486 +0,0 @@ | |||||||
| use std::fs; |  | ||||||
| use std::{collections::BTreeMap, sync::Arc}; |  | ||||||
| use tempfile::tempdir; |  | ||||||
| 
 |  | ||||||
| use async_trait::async_trait; |  | ||||||
| use kube::api::ObjectMeta; |  | ||||||
| use log::{debug, info}; |  | ||||||
| use serde::Serialize; |  | ||||||
| use std::process::Command; |  | ||||||
| 
 |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::grafana_default_dashboard::build_default_dashboard; |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability; |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanagers::{ |  | ||||||
|     Alertmanager, AlertmanagerSpec, |  | ||||||
| }; |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_grafana::{ |  | ||||||
|     Grafana, GrafanaDashboard, GrafanaDashboardSpec, GrafanaDatasource, GrafanaDatasourceConfig, |  | ||||||
|     GrafanaDatasourceSpec, GrafanaSpec, |  | ||||||
| }; |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_monitoring_stack::{ |  | ||||||
|     MonitoringStack, MonitoringStackSpec, |  | ||||||
| }; |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheus_rules::{ |  | ||||||
|     PrometheusRule, PrometheusRuleSpec, RuleGroup, |  | ||||||
| }; |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheuses::LabelSelector; |  | ||||||
| 
 |  | ||||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_service_monitor::{ |  | ||||||
|     ServiceMonitor, ServiceMonitorSpec, |  | ||||||
| }; |  | ||||||
| use crate::score::Score; |  | ||||||
| use crate::topology::oberservability::monitoring::AlertReceiver; |  | ||||||
| use crate::topology::{K8sclient, Topology, k8s::K8sClient}; |  | ||||||
| use crate::{ |  | ||||||
|     data::Version, |  | ||||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, |  | ||||||
|     inventory::Inventory, |  | ||||||
| }; |  | ||||||
| use harmony_types::id::Id; |  | ||||||
| 
 |  | ||||||
| use super::prometheus::PrometheusApplicationMonitoring; |  | ||||||
| 
 |  | ||||||
| #[derive(Clone, Debug, Serialize)] |  | ||||||
| pub struct RHOBAlertingScore { |  | ||||||
|     pub sender: RHOBObservability, |  | ||||||
|     pub receivers: Vec<Box<dyn AlertReceiver<RHOBObservability>>>, |  | ||||||
|     pub service_monitors: Vec<ServiceMonitor>, |  | ||||||
|     pub prometheus_rules: Vec<RuleGroup>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl<T: Topology + K8sclient + PrometheusApplicationMonitoring<RHOBObservability>> Score<T> |  | ||||||
|     for RHOBAlertingScore |  | ||||||
| { |  | ||||||
|     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { |  | ||||||
|         Box::new(RHOBAlertingInterpret { |  | ||||||
|             sender: self.sender.clone(), |  | ||||||
|             receivers: self.receivers.clone(), |  | ||||||
|             service_monitors: self.service_monitors.clone(), |  | ||||||
|             prometheus_rules: self.prometheus_rules.clone(), |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn name(&self) -> String { |  | ||||||
|         "RHOB alerting [RHOBAlertingScore]".into() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Clone, Debug)] |  | ||||||
| pub struct RHOBAlertingInterpret { |  | ||||||
|     pub sender: RHOBObservability, |  | ||||||
|     pub receivers: Vec<Box<dyn AlertReceiver<RHOBObservability>>>, |  | ||||||
|     pub service_monitors: Vec<ServiceMonitor>, |  | ||||||
|     pub prometheus_rules: Vec<RuleGroup>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[async_trait] |  | ||||||
| impl<T: Topology + K8sclient + PrometheusApplicationMonitoring<RHOBObservability>> Interpret<T> |  | ||||||
|     for RHOBAlertingInterpret |  | ||||||
| { |  | ||||||
|     async fn execute( |  | ||||||
|         &self, |  | ||||||
|         _inventory: &Inventory, |  | ||||||
|         topology: &T, |  | ||||||
|     ) -> Result<Outcome, InterpretError> { |  | ||||||
|         let client = topology.k8s_client().await.unwrap(); |  | ||||||
|         self.ensure_grafana_operator().await?; |  | ||||||
|         self.install_prometheus(&client).await?; |  | ||||||
|         self.install_client_kube_metrics().await?; |  | ||||||
|         self.install_grafana(&client).await?; |  | ||||||
|         self.install_receivers(&self.sender, &self.receivers) |  | ||||||
|             .await?; |  | ||||||
|         self.install_rules(&self.prometheus_rules, &client).await?; |  | ||||||
|         self.install_monitors(self.service_monitors.clone(), &client) |  | ||||||
|             .await?; |  | ||||||
|         Ok(Outcome::success( |  | ||||||
|             "K8s monitoring components installed".to_string(), |  | ||||||
|         )) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn get_name(&self) -> InterpretName { |  | ||||||
|         InterpretName::RHOBAlerting |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn get_version(&self) -> Version { |  | ||||||
|         todo!() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn get_status(&self) -> InterpretStatus { |  | ||||||
|         todo!() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn get_children(&self) -> Vec<Id> { |  | ||||||
|         todo!() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl RHOBAlertingInterpret { |  | ||||||
|     async fn crd_exists(&self, crd: &str) -> bool { |  | ||||||
|         let status = Command::new("sh") |  | ||||||
|             .args(["-c", &format!("kubectl get crd -A | grep -i {crd}")]) |  | ||||||
|             .status() |  | ||||||
|             .map_err(|e| InterpretError::new(format!("could not connect to cluster: {}", e))) |  | ||||||
|             .unwrap(); |  | ||||||
| 
 |  | ||||||
|         status.success() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn install_chart( |  | ||||||
|         &self, |  | ||||||
|         chart_path: String, |  | ||||||
|         chart_name: String, |  | ||||||
|     ) -> Result<(), InterpretError> { |  | ||||||
|         let temp_dir = |  | ||||||
|             tempdir().map_err(|e| InterpretError::new(format!("Tempdir error: {}", e)))?; |  | ||||||
|         let temp_path = temp_dir.path().to_path_buf(); |  | ||||||
|         debug!("Using temp directory: {}", temp_path.display()); |  | ||||||
|         let chart = format!("{}/{}", chart_path, chart_name); |  | ||||||
|         let pull_output = Command::new("helm") |  | ||||||
|             .args(["pull", &chart, "--destination", temp_path.to_str().unwrap()]) |  | ||||||
|             .output() |  | ||||||
|             .map_err(|e| InterpretError::new(format!("Helm pull error: {}", e)))?; |  | ||||||
| 
 |  | ||||||
|         if !pull_output.status.success() { |  | ||||||
|             return Err(InterpretError::new(format!( |  | ||||||
|                 "Helm pull failed: {}", |  | ||||||
|                 String::from_utf8_lossy(&pull_output.stderr) |  | ||||||
|             ))); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let tgz_path = fs::read_dir(&temp_path) |  | ||||||
|             .unwrap() |  | ||||||
|             .filter_map(|entry| { |  | ||||||
|                 let entry = entry.ok()?; |  | ||||||
|                 let path = entry.path(); |  | ||||||
|                 if path.extension()? == "tgz" { |  | ||||||
|                     Some(path) |  | ||||||
|                 } else { |  | ||||||
|                     None |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|             .next() |  | ||||||
|             .ok_or_else(|| InterpretError::new("Could not find pulled Helm chart".into()))?; |  | ||||||
| 
 |  | ||||||
|         debug!("Installing chart from: {}", tgz_path.display()); |  | ||||||
| 
 |  | ||||||
|         let install_output = Command::new("helm") |  | ||||||
|             .args([ |  | ||||||
|                 "upgrade", |  | ||||||
|                 "--install", |  | ||||||
|                 &chart_name, |  | ||||||
|                 tgz_path.to_str().unwrap(), |  | ||||||
|                 "--namespace", |  | ||||||
|                 &self.sender.namespace.clone(), |  | ||||||
|                 "--create-namespace", |  | ||||||
|                 "--wait", |  | ||||||
|                 "--atomic", |  | ||||||
|             ]) |  | ||||||
|             .output() |  | ||||||
|             .map_err(|e| InterpretError::new(format!("Helm install error: {}", e)))?; |  | ||||||
| 
 |  | ||||||
|         if !install_output.status.success() { |  | ||||||
|             return Err(InterpretError::new(format!( |  | ||||||
|                 "Helm install failed: {}", |  | ||||||
|                 String::from_utf8_lossy(&install_output.stderr) |  | ||||||
|             ))); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         debug!( |  | ||||||
|             "Installed chart {}/{} in namespace: {}", |  | ||||||
|             &chart_path, |  | ||||||
|             &chart_name, |  | ||||||
|             self.sender.namespace.clone() |  | ||||||
|         ); |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn ensure_grafana_operator(&self) -> Result<Outcome, InterpretError> { |  | ||||||
|         let _ = Command::new("helm") |  | ||||||
|             .args([ |  | ||||||
|                 "repo", |  | ||||||
|                 "add", |  | ||||||
|                 "grafana-operator", |  | ||||||
|                 "https://grafana.github.io/helm-charts", |  | ||||||
|             ]) |  | ||||||
|             .output() |  | ||||||
|             .unwrap(); |  | ||||||
| 
 |  | ||||||
|         let _ = Command::new("helm") |  | ||||||
|             .args(["repo", "update"]) |  | ||||||
|             .output() |  | ||||||
|             .unwrap(); |  | ||||||
| 
 |  | ||||||
|         let output = Command::new("helm") |  | ||||||
|             .args([ |  | ||||||
|                 "install", |  | ||||||
|                 "grafana-operator", |  | ||||||
|                 "grafana-operator/grafana-operator", |  | ||||||
|                 "--namespace", |  | ||||||
|                 &self.sender.namespace.clone(), |  | ||||||
|                 "--create-namespace", |  | ||||||
|                 "--set", |  | ||||||
|                 "namespaceScope=true", |  | ||||||
|             ]) |  | ||||||
|             .output() |  | ||||||
|             .unwrap(); |  | ||||||
| 
 |  | ||||||
|         if !output.status.success() { |  | ||||||
|             return Err(InterpretError::new(format!( |  | ||||||
|                 "helm install failed:\nstdout: {}\nstderr: {}", |  | ||||||
|                 String::from_utf8_lossy(&output.stdout), |  | ||||||
|                 String::from_utf8_lossy(&output.stderr) |  | ||||||
|             ))); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(Outcome::success(format!( |  | ||||||
|             "installed grafana operator in ns {}", |  | ||||||
|             self.sender.namespace.clone() |  | ||||||
|         ))) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn install_prometheus(&self, client: &Arc<K8sClient>) -> Result<Outcome, InterpretError> { |  | ||||||
|         debug!( |  | ||||||
|             "installing crd-prometheuses in namespace {}", |  | ||||||
|             self.sender.namespace.clone() |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         let stack = MonitoringStack { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(format!("{}-monitoring", self.sender.namespace.clone()).into()), |  | ||||||
|                 namespace: Some(self.sender.namespace.clone()), |  | ||||||
|                 labels: Some([("coo".into(), "example".into())].into()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec: MonitoringStackSpec { |  | ||||||
|                 log_level: Some("debug".into()), |  | ||||||
|                 retention: Some("1d".into()), |  | ||||||
|                 resource_selector: Some(LabelSelector { |  | ||||||
|                     match_labels: [("app".into(), "demo".into())].into(), |  | ||||||
|                     ..Default::default() |  | ||||||
|                 }), |  | ||||||
|             }, |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         client |  | ||||||
|             .apply(&stack, Some(&self.sender.namespace.clone())) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| InterpretError::new(e.to_string()))?; |  | ||||||
|         info!("installed rhob monitoring stack",); |  | ||||||
|         Ok(Outcome::success(format!( |  | ||||||
|             "successfully deployed rhob-prometheus {:#?}", |  | ||||||
|             stack |  | ||||||
|         ))) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn install_alert_manager( |  | ||||||
|         &self, |  | ||||||
|         client: &Arc<K8sClient>, |  | ||||||
|     ) -> Result<Outcome, InterpretError> { |  | ||||||
|         let am = Alertmanager { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(self.sender.namespace.clone()), |  | ||||||
|                 labels: Some(std::collections::BTreeMap::from([( |  | ||||||
|                     "alertmanagerConfig".to_string(), |  | ||||||
|                     "enabled".to_string(), |  | ||||||
|                 )])), |  | ||||||
|                 namespace: Some(self.sender.namespace.clone()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec: AlertmanagerSpec::default(), |  | ||||||
|         }; |  | ||||||
|         client |  | ||||||
|             .apply(&am, Some(&self.sender.namespace.clone())) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| InterpretError::new(e.to_string()))?; |  | ||||||
|         Ok(Outcome::success(format!( |  | ||||||
|             "successfully deployed service monitor {:#?}", |  | ||||||
|             am.metadata.name |  | ||||||
|         ))) |  | ||||||
|     } |  | ||||||
|     async fn install_monitors( |  | ||||||
|         &self, |  | ||||||
|         mut monitors: Vec<ServiceMonitor>, |  | ||||||
|         client: &Arc<K8sClient>, |  | ||||||
|     ) -> Result<Outcome, InterpretError> { |  | ||||||
|         let default_service_monitor = ServiceMonitor { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(self.sender.namespace.clone()), |  | ||||||
|                 labels: Some(std::collections::BTreeMap::from([ |  | ||||||
|                     ("alertmanagerConfig".to_string(), "enabled".to_string()), |  | ||||||
|                     ("client".to_string(), "prometheus".to_string()), |  | ||||||
|                     ( |  | ||||||
|                         "app.kubernetes.io/name".to_string(), |  | ||||||
|                         "kube-state-metrics".to_string(), |  | ||||||
|                     ), |  | ||||||
|                 ])), |  | ||||||
|                 namespace: Some(self.sender.namespace.clone()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec: ServiceMonitorSpec::default(), |  | ||||||
|         }; |  | ||||||
|         monitors.push(default_service_monitor); |  | ||||||
|         for monitor in monitors.iter() { |  | ||||||
|             client |  | ||||||
|                 .apply(monitor, Some(&self.sender.namespace.clone())) |  | ||||||
|                 .await |  | ||||||
|                 .map_err(|e| InterpretError::new(e.to_string()))?; |  | ||||||
|         } |  | ||||||
|         Ok(Outcome::success( |  | ||||||
|             "succesfully deployed service monitors".to_string(), |  | ||||||
|         )) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn install_rules( |  | ||||||
|         &self, |  | ||||||
|         #[allow(clippy::ptr_arg)] rules: &Vec<RuleGroup>, |  | ||||||
|         client: &Arc<K8sClient>, |  | ||||||
|     ) -> Result<Outcome, InterpretError> { |  | ||||||
|         let mut prom_rule_spec = PrometheusRuleSpec { |  | ||||||
|             groups: rules.clone(), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         let default_rules_group = RuleGroup { |  | ||||||
|             name: "default-rules".to_string(), |  | ||||||
|             rules: crate::modules::monitoring::kube_prometheus::crd::rhob_default_rules::build_default_application_rules(), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         prom_rule_spec.groups.push(default_rules_group); |  | ||||||
|         let prom_rules = PrometheusRule { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(self.sender.namespace.clone()), |  | ||||||
|                 labels: Some(std::collections::BTreeMap::from([ |  | ||||||
|                     ("alertmanagerConfig".to_string(), "enabled".to_string()), |  | ||||||
|                     ("role".to_string(), "prometheus-rule".to_string()), |  | ||||||
|                 ])), |  | ||||||
|                 namespace: Some(self.sender.namespace.clone()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec: prom_rule_spec, |  | ||||||
|         }; |  | ||||||
|         client |  | ||||||
|             .apply(&prom_rules, Some(&self.sender.namespace.clone())) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| InterpretError::new(e.to_string()))?; |  | ||||||
|         Ok(Outcome::success(format!( |  | ||||||
|             "successfully deployed rules {:#?}", |  | ||||||
|             prom_rules.metadata.name |  | ||||||
|         ))) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn install_client_kube_metrics(&self) -> Result<Outcome, InterpretError> { |  | ||||||
|         self.install_chart( |  | ||||||
|             "oci://hub.nationtech.io/harmony".to_string(), |  | ||||||
|             "nt-kube-metrics".to_string(), |  | ||||||
|         ) |  | ||||||
|         .await?; |  | ||||||
|         Ok(Outcome::success(format!( |  | ||||||
|             "Installed client kube metrics in ns {}", |  | ||||||
|             &self.sender.namespace.clone() |  | ||||||
|         ))) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn install_grafana(&self, client: &Arc<K8sClient>) -> Result<Outcome, InterpretError> { |  | ||||||
|         let mut label = BTreeMap::new(); |  | ||||||
|         label.insert("dashboards".to_string(), "grafana".to_string()); |  | ||||||
|         let labels = LabelSelector { |  | ||||||
|             match_labels: label.clone(), |  | ||||||
|             match_expressions: vec![], |  | ||||||
|         }; |  | ||||||
|         let mut json_data = BTreeMap::new(); |  | ||||||
|         json_data.insert("timeInterval".to_string(), "5s".to_string()); |  | ||||||
|         let namespace = self.sender.namespace.clone(); |  | ||||||
| 
 |  | ||||||
|         let json = build_default_dashboard(&namespace); |  | ||||||
| 
 |  | ||||||
|         let graf_data_source = GrafanaDatasource { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(format!( |  | ||||||
|                     "grafana-datasource-{}", |  | ||||||
|                     self.sender.namespace.clone() |  | ||||||
|                 )), |  | ||||||
|                 namespace: Some(self.sender.namespace.clone()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec: GrafanaDatasourceSpec { |  | ||||||
|                 instance_selector: labels.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", self.sender.namespace.clone()), |  | ||||||
|                     r#type: "prometheus".to_string(), |  | ||||||
|                     url: format!( |  | ||||||
|                         "http://prometheus-operated.{}.svc.cluster.local:9090", |  | ||||||
|                         self.sender.namespace.clone() |  | ||||||
|                     ), |  | ||||||
|                 }, |  | ||||||
|             }, |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         client |  | ||||||
|             .apply(&graf_data_source, Some(&self.sender.namespace.clone())) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| InterpretError::new(e.to_string()))?; |  | ||||||
| 
 |  | ||||||
|         let graf_dashboard = GrafanaDashboard { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(format!( |  | ||||||
|                     "grafana-dashboard-{}", |  | ||||||
|                     self.sender.namespace.clone() |  | ||||||
|                 )), |  | ||||||
|                 namespace: Some(self.sender.namespace.clone()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec: GrafanaDashboardSpec { |  | ||||||
|                 resync_period: Some("30s".to_string()), |  | ||||||
|                 instance_selector: labels.clone(), |  | ||||||
|                 json, |  | ||||||
|             }, |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         client |  | ||||||
|             .apply(&graf_dashboard, Some(&self.sender.namespace.clone())) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| InterpretError::new(e.to_string()))?; |  | ||||||
| 
 |  | ||||||
|         let grafana = Grafana { |  | ||||||
|             metadata: ObjectMeta { |  | ||||||
|                 name: Some(format!("grafana-{}", self.sender.namespace.clone())), |  | ||||||
|                 namespace: Some(self.sender.namespace.clone()), |  | ||||||
|                 labels: Some(label.clone()), |  | ||||||
|                 ..Default::default() |  | ||||||
|             }, |  | ||||||
|             spec: GrafanaSpec { |  | ||||||
|                 config: None, |  | ||||||
|                 admin_user: None, |  | ||||||
|                 admin_password: None, |  | ||||||
|                 ingress: None, |  | ||||||
|                 persistence: None, |  | ||||||
|                 resources: None, |  | ||||||
|             }, |  | ||||||
|         }; |  | ||||||
|         client |  | ||||||
|             .apply(&grafana, Some(&self.sender.namespace.clone())) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| InterpretError::new(e.to_string()))?; |  | ||||||
|         Ok(Outcome::success(format!( |  | ||||||
|             "successfully deployed grafana instance {:#?}", |  | ||||||
|             grafana.metadata.name |  | ||||||
|         ))) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     async fn install_receivers( |  | ||||||
|         &self, |  | ||||||
|         sender: &RHOBObservability, |  | ||||||
|         receivers: &Vec<Box<dyn AlertReceiver<RHOBObservability>>>, |  | ||||||
|     ) -> Result<Outcome, InterpretError> { |  | ||||||
|         for receiver in receivers.iter() { |  | ||||||
|             receiver.install(sender).await.map_err(|err| { |  | ||||||
|                 InterpretError::new(format!("failed to install receiver: {}", err)) |  | ||||||
|             })?; |  | ||||||
|         } |  | ||||||
|         Ok(Outcome::success("successfully deployed receivers".into())) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -155,24 +155,24 @@ pub fn cidrv4(input: TokenStream) -> TokenStream { | |||||||
| ///
 | ///
 | ||||||
| /// ```
 | /// ```
 | ||||||
| /// use harmony_types::net::Url;
 | /// use harmony_types::net::Url;
 | ||||||
| /// use harmony_macros::hurl;
 | /// use harmony_macros::remote_url;
 | ||||||
| ///
 | ///
 | ||||||
| /// let url = hurl!("https://example.com/path");
 | /// let remote_url = remote_url!("https://example.com/path");
 | ||||||
| ///
 | ///
 | ||||||
| /// let expected_url = url::Url::parse("https://example.com/path").unwrap();
 | /// let expected_url = url::Url::parse("https://example.com/path").unwrap();
 | ||||||
| /// assert!(matches!(url, Url::Url(expected_url)));
 | /// assert!(matches!(remote_url, Url::Url(expected_url)));
 | ||||||
| /// ```
 | /// ```
 | ||||||
| ///
 | ///
 | ||||||
| /// The following example will fail to compile:
 | /// The following example will fail to compile:
 | ||||||
| ///
 | ///
 | ||||||
| /// ```rust,compile_fail
 | /// ```rust,compile_fail
 | ||||||
| /// use harmony_macros::hurl;
 | /// use harmony_macros::remote_url;
 | ||||||
| ///
 | ///
 | ||||||
| /// // This is not a valid URL and will cause a compilation error.
 | /// // This is not a valid URL and will cause a compilation error.
 | ||||||
| /// let _invalid = hurl!("not a valid url");
 | /// let _invalid = remote_url!("not a valid url");
 | ||||||
| /// ```
 | /// ```
 | ||||||
| #[proc_macro] | #[proc_macro] | ||||||
| pub fn hurl(input: TokenStream) -> TokenStream { | pub fn remote_url(input: TokenStream) -> TokenStream { | ||||||
|     let input_lit = parse_macro_input!(input as LitStr); |     let input_lit = parse_macro_input!(input as LitStr); | ||||||
|     let url_str = input_lit.value(); |     let url_str = input_lit.value(); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -53,7 +53,7 @@ pub type IpAddress = std::net::IpAddr; | |||||||
| 
 | 
 | ||||||
| /// Represents a URL, which can either be a remote URL or a local file path.
 | /// Represents a URL, which can either be a remote URL or a local file path.
 | ||||||
| ///
 | ///
 | ||||||
| /// For convenience, the `harmony_macros` crate provides `hurl!` and `local_folder!`
 | /// For convenience, the `harmony_macros` crate provides `remote_url!` and `local_folder!`
 | ||||||
| /// macros to construct `Url` variants from string literals.
 | /// macros to construct `Url` variants from string literals.
 | ||||||
| ///
 | ///
 | ||||||
| /// # Examples
 | /// # Examples
 | ||||||
| @ -67,10 +67,10 @@ pub type IpAddress = std::net::IpAddr; | |||||||
| /// // The `use` statement below is for the doc test. In a real project,
 | /// // The `use` statement below is for the doc test. In a real project,
 | ||||||
| /// // you would use `use harmony_types::Url;`
 | /// // you would use `use harmony_types::Url;`
 | ||||||
| /// # use harmony_types::net::Url;
 | /// # use harmony_types::net::Url;
 | ||||||
| /// let url = Url::Url(url::Url::parse("https://example.com").unwrap());
 | /// let remote_url = Url::Url(url::Url::parse("https://example.com").unwrap());
 | ||||||
| /// let local_path = Url::LocalFolder("/var/data".to_string());
 | /// let local_path = Url::LocalFolder("/var/data".to_string());
 | ||||||
| ///
 | ///
 | ||||||
| /// assert!(matches!(url, Url::Url(_)));
 | /// assert!(matches!(remote_url, Url::Url(_)));
 | ||||||
| /// assert!(matches!(local_path, Url::LocalFolder(_)));
 | /// assert!(matches!(local_path, Url::LocalFolder(_)));
 | ||||||
| /// ```
 | /// ```
 | ||||||
| ///
 | ///
 | ||||||
| @ -79,10 +79,10 @@ pub type IpAddress = std::net::IpAddr; | |||||||
| /// If `harmony_macros` is a dependency, you can create `Url`s more concisely.
 | /// If `harmony_macros` is a dependency, you can create `Url`s more concisely.
 | ||||||
| ///
 | ///
 | ||||||
| /// ```rust,ignore
 | /// ```rust,ignore
 | ||||||
| /// use harmony_macros::{hurl, local_folder};
 | /// use harmony_macros::{remote_url, local_folder};
 | ||||||
| /// use harmony_types::Url;
 | /// use harmony_types::Url;
 | ||||||
| ///
 | ///
 | ||||||
| /// let hurl = hurl!("https://example.com");
 | /// let remote_url = remote_url!("https://example.com");
 | ||||||
| /// let local_path = local_folder!("/var/data");
 | /// let local_path = local_folder!("/var/data");
 | ||||||
| /// ```
 | /// ```
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user