refactor/ns #74
							
								
								
									
										789
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										789
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										22
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								Cargo.toml
									
									
									
									
									
								
							| @ -24,19 +24,31 @@ log = "0.4" | |||||||
| env_logger = "0.11" | env_logger = "0.11" | ||||||
| derive-new = "0.7" | derive-new = "0.7" | ||||||
| async-trait = "0.1" | async-trait = "0.1" | ||||||
| tokio = { version = "1.40", features = ["io-std", "fs", "macros", "rt-multi-thread"] } | tokio = { version = "1.40", features = [ | ||||||
|  |   "io-std", | ||||||
|  |   "fs", | ||||||
|  |   "macros", | ||||||
|  |   "rt-multi-thread", | ||||||
|  | ] } | ||||||
| cidr = { features = ["serde"], version = "0.2" } | cidr = { features = ["serde"], version = "0.2" } | ||||||
| russh = "0.45" | russh = "0.45" | ||||||
| russh-keys = "0.45" | russh-keys = "0.45" | ||||||
| rand = "0.8" | rand = "0.8" | ||||||
| url = "2.5" | url = "2.5" | ||||||
| kube = "0.98" | kube = { version = "1.1.0", features = [ | ||||||
| k8s-openapi = { version = "0.24", features = ["v1_30"] } |   "config", | ||||||
|  |   "client", | ||||||
|  |   "runtime", | ||||||
|  |   "rustls-tls", | ||||||
|  |   "ws", | ||||||
|  |   "jsonpatch", | ||||||
|  | ] } | ||||||
|  | k8s-openapi = { version = "0.25", features = ["v1_30"] } | ||||||
| serde_yaml = "0.9" | serde_yaml = "0.9" | ||||||
| serde-value = "0.7" | serde-value = "0.7" | ||||||
| http = "1.2" | http = "1.2" | ||||||
| inquire = "0.7" | inquire = "0.7" | ||||||
| convert_case =  "0.8" | convert_case = "0.8" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| similar = "2" | similar = "2" | ||||||
| uuid = { version = "1.11", features = [ "v4", "fast-rng", "macro-diagnostics" ] } | uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] } | ||||||
|  | |||||||
							
								
								
									
										78
									
								
								adr/013-monitoring-notifications.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								adr/013-monitoring-notifications.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | |||||||
|  | # Architecture Decision Record: Monitoring Notifications | ||||||
|  | 
 | ||||||
|  | Initial Author: Taha Hawa | ||||||
|  | 
 | ||||||
|  | Initial Date: 2025-06-26 | ||||||
|  | 
 | ||||||
|  | Last Updated Date: 2025-06-26 | ||||||
|  | 
 | ||||||
|  | ## Status | ||||||
|  | 
 | ||||||
|  | Proposed | ||||||
|  | 
 | ||||||
|  | ## Context | ||||||
|  | 
 | ||||||
|  | We need to send notifications (typically from AlertManager/Prometheus) and we need to receive said notifications on mobile devices for sure in some way, whether it's push messages, SMS, phone call, email, etc or all of the above. | ||||||
|  | 
 | ||||||
|  | ## Decision | ||||||
|  | 
 | ||||||
|  | We should go with https://ntfy.sh except host it ourselves. | ||||||
|  | 
 | ||||||
|  | `ntfy` is an open source solution written in Go that has the features we need. | ||||||
|  | 
 | ||||||
|  | ## Rationale | ||||||
|  | 
 | ||||||
|  | `ntfy` has pretty much everything we need (push notifications, email forwarding, receives via webhook), and nothing/not much we don't. Good fit, lightweight. | ||||||
|  | 
 | ||||||
|  | ## Consequences | ||||||
|  | 
 | ||||||
|  | Pros: | ||||||
|  | 
 | ||||||
|  | - topics, with ACLs | ||||||
|  | - lightweight | ||||||
|  | - reliable | ||||||
|  | - easy to configure | ||||||
|  | - mobile app | ||||||
|  |   - the mobile app can listen via websocket, poll, or receive via Firebase/GCM on Android, or similar on iOS. | ||||||
|  | - Forward to email | ||||||
|  | - Text-to-Speech phone call messages using Twilio integration | ||||||
|  | - Operates based on simple HTTP requests/Webhooks, easily usable via AlertManager | ||||||
|  | 
 | ||||||
|  | Cons: | ||||||
|  | 
 | ||||||
|  | - No SMS pushes | ||||||
|  | - SQLite DB, makes it harder to HA/scale | ||||||
|  | 
 | ||||||
|  | ## Alternatives considered | ||||||
|  | 
 | ||||||
|  | [AWS SNS](https://aws.amazon.com/sns/): | ||||||
|  | Pros: | ||||||
|  | 
 | ||||||
|  | - highly reliable | ||||||
|  | - no hosting needed | ||||||
|  | 
 | ||||||
|  | Cons: | ||||||
|  | 
 | ||||||
|  | - no control, not self hosted | ||||||
|  | - costs (per usage) | ||||||
|  | 
 | ||||||
|  | [Apprise](https://github.com/caronc/apprise): | ||||||
|  | Pros: | ||||||
|  | 
 | ||||||
|  | - Way more ways of sending notifications | ||||||
|  | - Can use ntfy as one of the backends/ways of sending | ||||||
|  | 
 | ||||||
|  | Cons: | ||||||
|  | 
 | ||||||
|  | - Way too overkill for what we need in terms of features | ||||||
|  | 
 | ||||||
|  | [Gotify](https://github.com/gotify/server): | ||||||
|  | Pros: | ||||||
|  | 
 | ||||||
|  | - simple, lightweight, golang, etc | ||||||
|  | 
 | ||||||
|  | Cons: | ||||||
|  | 
 | ||||||
|  | - Pushes topics are per-user | ||||||
|  | 
 | ||||||
|  | ## Additional Notes | ||||||
| @ -14,8 +14,8 @@ harmony_macros = { path = "../../harmony_macros" } | |||||||
| log = { workspace = true } | log = { workspace = true } | ||||||
| env_logger = { workspace = true } | env_logger = { workspace = true } | ||||||
| url = { workspace = true } | url = { workspace = true } | ||||||
| kube = "0.98.0" | kube = "1.1.0" | ||||||
| k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] } | k8s-openapi = { version = "0.25.0", features = ["v1_30"] } | ||||||
| http = "1.2.0" | http = "1.2.0" | ||||||
| serde_yaml = "0.9.34" | serde_yaml = "0.9.34" | ||||||
| inquire.workspace = true | inquire.workspace = true | ||||||
|  | |||||||
| @ -8,5 +8,6 @@ license.workspace = true | |||||||
| [dependencies] | [dependencies] | ||||||
| harmony = { version = "0.1.0", path = "../../harmony" } | harmony = { version = "0.1.0", path = "../../harmony" } | ||||||
| harmony_cli = { version = "0.1.0", path = "../../harmony_cli" } | harmony_cli = { version = "0.1.0", path = "../../harmony_cli" } | ||||||
|  | harmony_macros = { version = "0.1.0", path = "../../harmony_macros" } | ||||||
| tokio.workspace = true | tokio.workspace = true | ||||||
| url.workspace = true | url.workspace = true | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | use std::collections::HashMap; | ||||||
|  | 
 | ||||||
| use harmony::{ | use harmony::{ | ||||||
|     inventory::Inventory, |     inventory::Inventory, | ||||||
|     maestro::Maestro, |     maestro::Maestro, | ||||||
| @ -41,9 +43,30 @@ async fn main() { | |||||||
|         ], |         ], | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     let service_monitor_endpoint = ServiceMonitorEndpoint { | ||||||
|  |         port: Some("80".to_string()), | ||||||
|  |         path: "/metrics".to_string(), | ||||||
|  |         scheme: HTTPScheme::HTTP, | ||||||
|  |         ..Default::default() | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let service_monitor = ServiceMonitor { | ||||||
|  |         name: "test-service-monitor".to_string(), | ||||||
|  |         selector: Selector { | ||||||
|  |             match_labels: HashMap::new(), | ||||||
|  |             match_expressions: vec![MatchExpression { | ||||||
|  |                 key: "test".to_string(), | ||||||
|  |                 operator: Operator::In, | ||||||
|  |                 values: vec!["test-service".to_string()], | ||||||
|  |             }], | ||||||
|  |         }, | ||||||
|  |         endpoints: vec![service_monitor_endpoint], | ||||||
|  |         ..Default::default() | ||||||
|  |     }; | ||||||
|     let alerting_score = HelmPrometheusAlertingScore { |     let alerting_score = HelmPrometheusAlertingScore { | ||||||
|         receivers: vec![Box::new(discord_receiver)], |         receivers: vec![Box::new(discord_receiver)], | ||||||
|         rules: vec![Box::new(additional_rules), Box::new(additional_rules2)], |         rules: vec![Box::new(additional_rules), Box::new(additional_rules2)], | ||||||
|  |         service_monitors: vec![service_monitor], | ||||||
|     }; |     }; | ||||||
|     let mut maestro = Maestro::<K8sAnywhereTopology>::initialize( |     let mut maestro = Maestro::<K8sAnywhereTopology>::initialize( | ||||||
|         Inventory::autoload(), |         Inventory::autoload(), | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								examples/ntfy/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								examples/ntfy/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | [package] | ||||||
|  | name = "example-ntfy" | ||||||
|  | edition = "2024" | ||||||
|  | version.workspace = true | ||||||
|  | readme.workspace = true | ||||||
|  | license.workspace = true | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | harmony = { version = "0.1.0", path = "../../harmony" } | ||||||
|  | harmony_cli = { version = "0.1.0", path = "../../harmony_cli" } | ||||||
|  | tokio.workspace = true | ||||||
|  | url.workspace = true | ||||||
							
								
								
									
										19
									
								
								examples/ntfy/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								examples/ntfy/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | use harmony::{ | ||||||
|  |     inventory::Inventory, maestro::Maestro, modules::monitoring::ntfy::ntfy::NtfyScore, | ||||||
|  |     topology::K8sAnywhereTopology, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |     let mut maestro = Maestro::<K8sAnywhereTopology>::initialize( | ||||||
|  |         Inventory::autoload(), | ||||||
|  |         K8sAnywhereTopology::from_env(), | ||||||
|  |     ) | ||||||
|  |     .await | ||||||
|  |     .unwrap(); | ||||||
|  | 
 | ||||||
|  |     maestro.register_all(vec![Box::new(NtfyScore { | ||||||
|  |         namespace: "monitoring".to_string(), | ||||||
|  |     })]); | ||||||
|  |     harmony_cli::init(maestro, None).await.unwrap(); | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								examples/rust/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								examples/rust/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | [package] | ||||||
|  | name = "example-rust" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2024" | ||||||
|  | 
 | ||||||
|  | [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 } | ||||||
							
								
								
									
										20
									
								
								examples/rust/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								examples/rust/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | use harmony::{ | ||||||
|  |     inventory::Inventory, | ||||||
|  |     maestro::Maestro, | ||||||
|  |     modules::application::{RustWebappScore, features::ContinuousDelivery}, | ||||||
|  |     topology::{K8sAnywhereTopology, Url}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |     let app = RustWebappScore { | ||||||
|  |         name: "Example Rust Webapp".to_string(), | ||||||
|  |         domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), | ||||||
|  |         features: vec![Box::new(ContinuousDelivery {})], | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let topology = K8sAnywhereTopology::from_env(); | ||||||
|  |     let mut maestro = Maestro::new(Inventory::autoload(), topology); | ||||||
|  |     maestro.register_all(vec![Box::new(app)]); | ||||||
|  |     harmony_cli::init(maestro, None).await.unwrap(); | ||||||
|  | } | ||||||
| @ -10,9 +10,9 @@ publish = false | |||||||
| harmony = { path = "../../harmony" } | harmony = { path = "../../harmony" } | ||||||
| harmony_tui = { path = "../../harmony_tui" } | harmony_tui = { path = "../../harmony_tui" } | ||||||
| harmony_types = { path = "../../harmony_types" } | harmony_types = { path = "../../harmony_types" } | ||||||
|  | harmony_macros = { path = "../../harmony_macros" } | ||||||
| cidr = { workspace = true } | cidr = { workspace = true } | ||||||
| tokio = { workspace = true } | tokio = { workspace = true } | ||||||
| harmony_macros = { path = "../../harmony_macros" } |  | ||||||
| log = { workspace = true } | log = { workspace = true } | ||||||
| env_logger = { workspace = true } | env_logger = { workspace = true } | ||||||
| url = { workspace = true } | url = { workspace = true } | ||||||
|  | |||||||
| @ -54,3 +54,6 @@ fqdn = { version = "0.4.6", features = [ | |||||||
| temp-dir = "0.1.14" | temp-dir = "0.1.14" | ||||||
| dyn-clone = "1.0.19" | dyn-clone = "1.0.19" | ||||||
| similar.workspace = true | similar.workspace = true | ||||||
|  | futures-util = "0.3.31" | ||||||
|  | tokio-util = "0.7.15" | ||||||
|  | strum = { version = "0.27.1", features = ["derive"] } | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ pub enum InterpretName { | |||||||
|     OPNSense, |     OPNSense, | ||||||
|     K3dInstallation, |     K3dInstallation, | ||||||
|     TenantInterpret, |     TenantInterpret, | ||||||
|  |     Application, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl std::fmt::Display for InterpretName { | impl std::fmt::Display for InterpretName { | ||||||
| @ -37,6 +38,7 @@ impl std::fmt::Display for InterpretName { | |||||||
|             InterpretName::OPNSense => f.write_str("OPNSense"), |             InterpretName::OPNSense => f.write_str("OPNSense"), | ||||||
|             InterpretName::K3dInstallation => f.write_str("K3dInstallation"), |             InterpretName::K3dInstallation => f.write_str("K3dInstallation"), | ||||||
|             InterpretName::TenantInterpret => f.write_str("Tenant"), |             InterpretName::TenantInterpret => f.write_str("Tenant"), | ||||||
|  |             InterpretName::Application => f.write_str("Application"), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -124,3 +126,11 @@ impl From<kube::Error> for InterpretError { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | impl From<String> for InterpretError { | ||||||
|  |     fn from(value: String) -> Self { | ||||||
|  |         Self { | ||||||
|  |             msg: format!("InterpretError : {value}"), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -34,6 +34,17 @@ pub struct Inventory { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Inventory { | impl Inventory { | ||||||
|  |     pub fn empty() -> Self { | ||||||
|  |         Self { | ||||||
|  |             location: Location::new("Empty".to_string(), "location".to_string()), | ||||||
|  |             switch: vec![], | ||||||
|  |             firewall: vec![], | ||||||
|  |             worker_host: vec![], | ||||||
|  |             storage_host: vec![], | ||||||
|  |             control_plane_host: vec![], | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub fn autoload() -> Self { |     pub fn autoload() -> Self { | ||||||
|         Self { |         Self { | ||||||
|             location: Location::test_building(), |             location: Location::test_building(), | ||||||
|  | |||||||
| @ -46,7 +46,7 @@ pub struct HAClusterTopology { | |||||||
| #[async_trait] | #[async_trait] | ||||||
| impl Topology for HAClusterTopology { | impl Topology for HAClusterTopology { | ||||||
|     fn name(&self) -> &str { |     fn name(&self) -> &str { | ||||||
|         todo!() |         "HAClusterTopology" | ||||||
|     } |     } | ||||||
|     async fn ensure_ready(&self) -> Result<Outcome, InterpretError> { |     async fn ensure_ready(&self) -> Result<Outcome, InterpretError> { | ||||||
|         todo!( |         todo!( | ||||||
|  | |||||||
| @ -1,14 +1,21 @@ | |||||||
| use derive_new::new; | use derive_new::new; | ||||||
| use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope}; | use futures_util::StreamExt; | ||||||
|  | use k8s_openapi::{ | ||||||
|  |     ClusterResourceScope, NamespaceResourceScope, | ||||||
|  |     api::{apps::v1::Deployment, core::v1::Pod}, | ||||||
|  | }; | ||||||
|  | use kube::runtime::conditions; | ||||||
|  | use kube::runtime::wait::await_condition; | ||||||
| use kube::{ | use kube::{ | ||||||
|     Api, Client, Config, Error, Resource, |     Client, Config, Error, Resource, | ||||||
|     api::{Patch, PatchParams}, |     api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt}, | ||||||
|     config::{KubeConfigOptions, Kubeconfig}, |     config::{KubeConfigOptions, Kubeconfig}, | ||||||
|     core::ErrorResponse, |     core::ErrorResponse, | ||||||
|  |     runtime::reflector::Lookup, | ||||||
| }; | }; | ||||||
| use log::{debug, error, trace}; | use log::{debug, error, trace}; | ||||||
| use serde::de::DeserializeOwned; | use serde::de::DeserializeOwned; | ||||||
| use similar::TextDiff; | use similar::{DiffableStr, TextDiff}; | ||||||
| 
 | 
 | ||||||
| #[derive(new, Clone)] | #[derive(new, Clone)] | ||||||
| pub struct K8sClient { | pub struct K8sClient { | ||||||
| @ -33,6 +40,88 @@ impl K8sClient { | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub async fn wait_until_deployment_ready( | ||||||
|  |         &self, | ||||||
|  |         name: String, | ||||||
|  |         namespace: Option<&str>, | ||||||
|  |         timeout: Option<u64>, | ||||||
|  |     ) -> Result<(), String> { | ||||||
|  |         let api: Api<Deployment>; | ||||||
|  | 
 | ||||||
|  |         if let Some(ns) = namespace { | ||||||
|  |             api = Api::namespaced(self.client.clone(), ns); | ||||||
|  |         } else { | ||||||
|  |             api = Api::default_namespaced(self.client.clone()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed()); | ||||||
|  |         let t = if let Some(t) = timeout { t } else { 300 }; | ||||||
|  |         let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await; | ||||||
|  | 
 | ||||||
|  |         if let Ok(r) = res { | ||||||
|  |             return Ok(()); | ||||||
|  |         } else { | ||||||
|  |             return Err("timed out while waiting for deployment".to_string()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Will execute a command in the first pod found that matches the label `app.kubernetes.io/name={name}`
 | ||||||
|  |     pub async fn exec_app( | ||||||
|  |         &self, | ||||||
|  |         name: String, | ||||||
|  |         namespace: Option<&str>, | ||||||
|  |         command: Vec<&str>, | ||||||
|  |     ) -> Result<(), String> { | ||||||
|  |         let api: Api<Pod>; | ||||||
|  | 
 | ||||||
|  |         if let Some(ns) = namespace { | ||||||
|  |             api = Api::namespaced(self.client.clone(), ns); | ||||||
|  |         } else { | ||||||
|  |             api = Api::default_namespaced(self.client.clone()); | ||||||
|  |         } | ||||||
|  |         let pod_list = api | ||||||
|  |             .list(&ListParams::default().labels(format!("app.kubernetes.io/name={name}").as_str())) | ||||||
|  |             .await | ||||||
|  |             .expect("couldn't get list of pods"); | ||||||
|  | 
 | ||||||
|  |         let res = api | ||||||
|  |             .exec( | ||||||
|  |                 pod_list | ||||||
|  |                     .items | ||||||
|  |                     .first() | ||||||
|  |                     .expect("couldn't get pod") | ||||||
|  |                     .name() | ||||||
|  |                     .expect("couldn't get pod name") | ||||||
|  |                     .into_owned() | ||||||
|  |                     .as_str(), | ||||||
|  |                 command, | ||||||
|  |                 &AttachParams::default(), | ||||||
|  |             ) | ||||||
|  |             .await; | ||||||
|  | 
 | ||||||
|  |         match res { | ||||||
|  |             Err(e) => return Err(e.to_string()), | ||||||
|  |             Ok(mut process) => { | ||||||
|  |                 let status = process | ||||||
|  |                     .take_status() | ||||||
|  |                     .expect("Couldn't get status") | ||||||
|  |                     .await | ||||||
|  |                     .expect("Couldn't unwrap status"); | ||||||
|  | 
 | ||||||
|  |                 if let Some(s) = status.status { | ||||||
|  |                     debug!("Status: {}", s); | ||||||
|  |                     if s == "Success" { | ||||||
|  |                         return Ok(()); | ||||||
|  |                     } else { | ||||||
|  |                         return Err(s); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     return Err("Couldn't get inner status of pod exec".to_string()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /// Apply a resource in namespace
 |     /// Apply a resource in namespace
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// See `kubectl apply` for more information on the expected behavior of this function
 |     /// See `kubectl apply` for more information on the expected behavior of this function
 | ||||||
|  | |||||||
| @ -1,7 +1,14 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use log::info; | use log::info; | ||||||
|  | use serde_json::Value; | ||||||
| 
 | 
 | ||||||
| use crate::{modules::application::ApplicationFeature, topology::Topology}; | use crate::{ | ||||||
|  |     data::Version, | ||||||
|  |     inventory::Inventory, | ||||||
|  |     modules::{application::ApplicationFeature, helm::chart::HelmChartScore}, | ||||||
|  |     score::Score, | ||||||
|  |     topology::{HelmCommand, Topology, Url}, | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| /// ContinuousDelivery in Harmony provides this functionality :
 | /// ContinuousDelivery in Harmony provides this functionality :
 | ||||||
| ///
 | ///
 | ||||||
| @ -30,13 +37,48 @@ use crate::{modules::application::ApplicationFeature, topology::Topology}; | |||||||
| /// - Harbor as artifact registru
 | /// - Harbor as artifact registru
 | ||||||
| /// - ArgoCD to install/upgrade/rollback/inspect k8s resources
 | /// - ArgoCD to install/upgrade/rollback/inspect k8s resources
 | ||||||
| /// - Kubernetes for runtime orchestration
 | /// - Kubernetes for runtime orchestration
 | ||||||
| #[derive(Debug, Default)] | #[derive(Debug, Default, Clone)] | ||||||
| pub struct ContinuousDelivery {} | pub struct ContinuousDelivery {} | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl<T: Topology + 'static> ApplicationFeature<T> for ContinuousDelivery { | impl<T: Topology + HelmCommand + 'static> ApplicationFeature<T> for ContinuousDelivery { | ||||||
|     async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { |     async fn ensure_installed(&self, topology: &T) -> Result<(), String> { | ||||||
|         info!("Installing ContinuousDelivery feature"); |         info!("Installing ContinuousDelivery feature"); | ||||||
|         todo!() |         let cd_server = HelmChartScore { | ||||||
|  |             namespace: todo!( | ||||||
|  |                 "ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now" | ||||||
|  |             ), | ||||||
|  |             release_name: todo!("argocd helm chart whatever"), | ||||||
|  |             chart_name: todo!(), | ||||||
|  |             chart_version: todo!(), | ||||||
|  |             values_overrides: todo!(), | ||||||
|  |             values_yaml: todo!(), | ||||||
|  |             create_namespace: todo!(), | ||||||
|  |             install_only: todo!(), | ||||||
|  |             repository: todo!(), | ||||||
|  |         }; | ||||||
|  |         let interpret = cd_server.create_interpret(); | ||||||
|  |         interpret.execute(&Inventory::empty(), topology); | ||||||
|  | 
 | ||||||
|  |         todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it
 | ||||||
|  |             2. Package app (docker image, helm chart) | ||||||
|  |             3. Push to registry if staging or prod | ||||||
|  |             4. Poke Argo | ||||||
|  |             5. Ensure app is up")
 | ||||||
|  |     } | ||||||
|  |     fn name(&self) -> String { | ||||||
|  |         "ContinuousDelivery".to_string() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /// For now this is entirely bound to K8s / ArgoCD, will have to be revisited when we support
 | ||||||
|  | /// more CD systems
 | ||||||
|  | pub struct CDApplicationConfig { | ||||||
|  |     version: Version, | ||||||
|  |     helm_chart_url: Url, | ||||||
|  |     values_overrides: Value, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub trait ContinuousDeliveryApplication { | ||||||
|  |     fn get_config(&self) -> CDApplicationConfig; | ||||||
|  | } | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ use crate::{ | |||||||
|     topology::{K8sclient, Topology}, |     topology::{K8sclient, Topology}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug, Clone)] | ||||||
| pub struct PublicEndpoint { | pub struct PublicEndpoint { | ||||||
|     application_port: u16, |     application_port: u16, | ||||||
| } | } | ||||||
| @ -36,4 +36,7 @@ impl<T: Topology + K8sclient + 'static> ApplicationFeature<T> for PublicEndpoint | |||||||
|         ); |         ); | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
|  |     fn name(&self) -> String { | ||||||
|  |         "PublicEndpoint".to_string() | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ use crate::{ | |||||||
|     topology::{HelmCommand, Topology}, |     topology::{HelmCommand, Topology}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Default)] | #[derive(Debug, Default, Clone)] | ||||||
| pub struct Monitoring {} | pub struct Monitoring {} | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| @ -15,4 +15,7 @@ impl<T: Topology + HelmCommand + 'static> ApplicationFeature<T> for Monitoring { | |||||||
|         info!("Ensuring monitoring is available for application"); |         info!("Ensuring monitoring is available for application"); | ||||||
|         todo!("create and execute k8s prometheus score, depends on Will's work") |         todo!("create and execute k8s prometheus score, depends on Will's work") | ||||||
|     } |     } | ||||||
|  |     fn name(&self) -> String { | ||||||
|  |         "Monitoring".to_string() | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,9 +1,11 @@ | |||||||
|  | mod feature; | ||||||
| pub mod features; | pub mod features; | ||||||
| mod rust; | mod rust; | ||||||
|  | pub use feature::*; | ||||||
|  | use log::info; | ||||||
| pub use rust::*; | pub use rust::*; | ||||||
| 
 | 
 | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use serde::Serialize; |  | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     data::{Id, Version}, |     data::{Id, Version}, | ||||||
| @ -12,9 +14,14 @@ use crate::{ | |||||||
|     topology::Topology, |     topology::Topology, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | pub trait Application: std::fmt::Debug + Send + Sync { | ||||||
|  |     fn name(&self) -> String; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct ApplicationInterpret<T: Topology + std::fmt::Debug> { | pub struct ApplicationInterpret<T: Topology + std::fmt::Debug> { | ||||||
|     features: Vec<Box<dyn ApplicationFeature<T>>>, |     features: Vec<Box<dyn ApplicationFeature<T>>>, | ||||||
|  |     application: Box<dyn Application>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| @ -22,17 +29,43 @@ impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> { | |||||||
|     async fn execute( |     async fn execute( | ||||||
|         &self, |         &self, | ||||||
|         _inventory: &Inventory, |         _inventory: &Inventory, | ||||||
|         _topology: &T, |         topology: &T, | ||||||
|     ) -> Result<Outcome, InterpretError> { |     ) -> Result<Outcome, InterpretError> { | ||||||
|         todo!() |         let app_name = self.application.name(); | ||||||
|  |         info!( | ||||||
|  |             "Preparing {} features [{}] for application {app_name}", | ||||||
|  |             self.features.len(), | ||||||
|  |             self.features | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|f| f.name()) | ||||||
|  |                 .collect::<Vec<String>>() | ||||||
|  |                 .join(", ") | ||||||
|  |         ); | ||||||
|  |         for feature in self.features.iter() { | ||||||
|  |             info!( | ||||||
|  |                 "Installing feature {} for application {app_name}", | ||||||
|  |                 feature.name() | ||||||
|  |             ); | ||||||
|  |             let _ = match feature.ensure_installed(topology).await { | ||||||
|  |                 Ok(()) => (), | ||||||
|  |                 Err(msg) => { | ||||||
|  |                     return Err(InterpretError::new(format!( | ||||||
|  |                         "Application Interpret failed to install feature : {msg}" | ||||||
|  |                     ))); | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         todo!( | ||||||
|  |             "Do I need to do anything more than this here?? I feel like the Application trait itself should expose something like ensure_ready but its becoming redundant. We'll see as this evolves." | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn get_name(&self) -> InterpretName { |     fn get_name(&self) -> InterpretName { | ||||||
|         todo!() |         InterpretName::Application | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn get_version(&self) -> Version { |     fn get_version(&self) -> Version { | ||||||
|         todo!() |         Version::from("1.0.0").unwrap() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn get_status(&self) -> InterpretStatus { |     fn get_status(&self) -> InterpretStatus { | ||||||
| @ -43,25 +76,3 @@ impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> { | |||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
 |  | ||||||
| /// ContinuousIntegration, ContinuousDelivery
 |  | ||||||
| #[async_trait] |  | ||||||
| pub trait ApplicationFeature<T: Topology>: std::fmt::Debug + Send + Sync { |  | ||||||
|     async fn ensure_installed(&self, topology: &T) -> Result<(), String>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl<T: Topology> Serialize for Box<dyn ApplicationFeature<T>> { |  | ||||||
|     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |  | ||||||
|     where |  | ||||||
|         S: serde::Serializer, |  | ||||||
|     { |  | ||||||
|         todo!() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl<T: Topology> Clone for Box<dyn ApplicationFeature<T>> { |  | ||||||
|     fn clone(&self) -> Self { |  | ||||||
|         todo!() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ use crate::{ | |||||||
|     topology::{Topology, Url}, |     topology::{Topology, Url}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::{ApplicationFeature, ApplicationInterpret}; | use super::{Application, ApplicationFeature, ApplicationInterpret}; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Serialize, Clone)] | #[derive(Debug, Serialize, Clone)] | ||||||
| pub struct RustWebappScore<T: Topology + Clone + Serialize> { | pub struct RustWebappScore<T: Topology + Clone + Serialize> { | ||||||
| @ -16,10 +16,26 @@ pub struct RustWebappScore<T: Topology + Clone + Serialize> { | |||||||
| 
 | 
 | ||||||
| impl<T: Topology + std::fmt::Debug + Clone + Serialize + 'static> Score<T> for RustWebappScore<T> { | impl<T: Topology + std::fmt::Debug + Clone + Serialize + 'static> Score<T> for RustWebappScore<T> { | ||||||
|     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { |     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { | ||||||
|         Box::new(ApplicationInterpret { features: todo!() }) |         Box::new(ApplicationInterpret { | ||||||
|  |             features: self.features.clone(), | ||||||
|  |             application: Box::new(RustWebapp { | ||||||
|  |                 name: self.name.clone(), | ||||||
|  |             }), | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn name(&self) -> String { |     fn name(&self) -> String { | ||||||
|         format!("{}-RustWebapp", self.name) |         format!("{}-RustWebapp", self.name) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | struct RustWebapp { | ||||||
|  |     name: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Application for RustWebapp { | ||||||
|  |     fn name(&self) -> String { | ||||||
|  |         self.name.clone() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| 
 | 
 | ||||||
| use crate::modules::monitoring::kube_prometheus::types::{ | use crate::modules::monitoring::kube_prometheus::types::{ | ||||||
|     AlertManagerAdditionalPromRules, AlertManagerChannelConfig, |     AlertManagerAdditionalPromRules, AlertManagerChannelConfig, ServiceMonitor, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone, Serialize)] | #[derive(Debug, Clone, Serialize)] | ||||||
| @ -25,6 +25,7 @@ pub struct KubePrometheusConfig { | |||||||
|     pub prometheus_operator: bool, |     pub prometheus_operator: bool, | ||||||
|     pub alert_receiver_configs: Vec<AlertManagerChannelConfig>, |     pub alert_receiver_configs: Vec<AlertManagerChannelConfig>, | ||||||
|     pub alert_rules: Vec<AlertManagerAdditionalPromRules>, |     pub alert_rules: Vec<AlertManagerAdditionalPromRules>, | ||||||
|  |     pub additional_service_monitors: Vec<ServiceMonitor>, | ||||||
| } | } | ||||||
| impl KubePrometheusConfig { | impl KubePrometheusConfig { | ||||||
|     pub fn new() -> Self { |     pub fn new() -> Self { | ||||||
| @ -48,6 +49,7 @@ impl KubePrometheusConfig { | |||||||
|             kube_scheduler: false, |             kube_scheduler: false, | ||||||
|             alert_receiver_configs: vec![], |             alert_receiver_configs: vec![], | ||||||
|             alert_rules: vec![], |             alert_rules: vec![], | ||||||
|  |             additional_service_monitors: vec![], | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -11,9 +11,7 @@ use std::{ | |||||||
| use crate::modules::{ | use crate::modules::{ | ||||||
|     helm::chart::HelmChartScore, |     helm::chart::HelmChartScore, | ||||||
|     monitoring::kube_prometheus::types::{ |     monitoring::kube_prometheus::types::{ | ||||||
|         AlertGroup, AlertManager, AlertManagerAdditionalPromRules, AlertManagerConfig, |         AlertGroup, AlertManager, AlertManagerAdditionalPromRules, AlertManagerConfig, AlertManagerRoute, AlertManagerSpec, AlertManagerValues, ConfigReloader, Limits, PrometheusConfig, Requests, Resources | ||||||
|         AlertManagerRoute, AlertManagerSpec, AlertManagerValues, ConfigReloader, Limits, Requests, |  | ||||||
|         Resources, |  | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -278,6 +276,22 @@ prometheusOperator: | |||||||
| "#,
 | "#,
 | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     let prometheus_config = | ||||||
|  |         crate::modules::monitoring::kube_prometheus::types::PrometheusConfigValues { | ||||||
|  |             prometheus: PrometheusConfig { | ||||||
|  |                 prometheus: bool::from_str(prometheus.as_str()).expect("couldn't parse bool"), | ||||||
|  |                 additional_service_monitors: config.additional_service_monitors.clone(), | ||||||
|  |             }, | ||||||
|  |         }; | ||||||
|  |     let prometheus_config_yaml = | ||||||
|  |         serde_yaml::to_string(&prometheus_config).expect("Failed to serialize YAML"); | ||||||
|  | 
 | ||||||
|  |     debug!( | ||||||
|  |         "serialized prometheus config: \n {:#}", | ||||||
|  |         prometheus_config_yaml | ||||||
|  |     ); | ||||||
|  |     values.push_str(&prometheus_config_yaml); | ||||||
|  | 
 | ||||||
|     // add required null receiver for prometheus alert manager
 |     // add required null receiver for prometheus alert manager
 | ||||||
|     let mut null_receiver = Mapping::new(); |     let mut null_receiver = Mapping::new(); | ||||||
|     null_receiver.insert( |     null_receiver.insert( | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ use serde::Serialize; | |||||||
| 
 | 
 | ||||||
| use super::{helm::config::KubePrometheusConfig, prometheus::Prometheus}; | use super::{helm::config::KubePrometheusConfig, prometheus::Prometheus}; | ||||||
| use crate::{ | use crate::{ | ||||||
|  |     modules::monitoring::kube_prometheus::types::ServiceMonitor, | ||||||
|     score::Score, |     score::Score, | ||||||
|     topology::{ |     topology::{ | ||||||
|         HelmCommand, Topology, |         HelmCommand, Topology, | ||||||
| @ -16,10 +17,16 @@ use crate::{ | |||||||
| pub struct HelmPrometheusAlertingScore { | pub struct HelmPrometheusAlertingScore { | ||||||
|     pub receivers: Vec<Box<dyn AlertReceiver<Prometheus>>>, |     pub receivers: Vec<Box<dyn AlertReceiver<Prometheus>>>, | ||||||
|     pub rules: Vec<Box<dyn AlertRule<Prometheus>>>, |     pub rules: Vec<Box<dyn AlertRule<Prometheus>>>, | ||||||
|  |     pub service_monitors: Vec<ServiceMonitor>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<T: Topology + HelmCommand + TenantManager> Score<T> for HelmPrometheusAlertingScore { | impl<T: Topology + HelmCommand + TenantManager> Score<T> for HelmPrometheusAlertingScore { | ||||||
|  | |||||||
|     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { |     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { | ||||||
|  |         let config = Arc::new(Mutex::new(KubePrometheusConfig::new())); | ||||||
|  |         config | ||||||
|  |             .try_lock() | ||||||
|  |             .expect("couldn't lock config") | ||||||
|  |             .additional_service_monitors = self.service_monitors.clone(); | ||||||
|         Box::new(AlertingInterpret { |         Box::new(AlertingInterpret { | ||||||
|             sender: Prometheus::new(), |             sender: Prometheus::new(), | ||||||
|             receivers: self.receivers.clone(), |             receivers: self.receivers.clone(), | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| use std::collections::BTreeMap; | use std::collections::{BTreeMap, HashMap}; | ||||||
| 
 | 
 | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| @ -85,3 +85,202 @@ pub struct AlertManagerAdditionalPromRules { | |||||||
| pub struct AlertGroup { | pub struct AlertGroup { | ||||||
|     pub groups: Vec<AlertManagerRuleGroup>, |     pub groups: Vec<AlertManagerRuleGroup>, | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | pub enum HTTPScheme { | ||||||
|  |     #[serde(rename = "http")] | ||||||
|  |     HTTP, | ||||||
|  |     #[serde(rename = "https")] | ||||||
|  |     HTTPS, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | pub enum Operator { | ||||||
|  |     In, | ||||||
|  |     NotIn, | ||||||
|  |     Exists, | ||||||
|  |     DoesNotExist, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct PrometheusConfigValues { | ||||||
|  |     pub prometheus: PrometheusConfig, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct PrometheusConfig { | ||||||
|  |     pub prometheus: bool, | ||||||
|  |     pub additional_service_monitors: Vec<ServiceMonitor>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct ServiceMonitorTLSConfig { | ||||||
|  |     // ## Path to the CA file
 | ||||||
|  |     // ##
 | ||||||
|  |     pub ca_file: Option<String>, | ||||||
|  | 
 | ||||||
|  |     // ## Path to client certificate file
 | ||||||
|  |     // ##
 | ||||||
|  |     pub cert_file: Option<String>, | ||||||
|  | 
 | ||||||
|  |     // ## Skip certificate verification
 | ||||||
|  |     // ##
 | ||||||
|  |     pub insecure_skip_verify: Option<bool>, | ||||||
|  | 
 | ||||||
|  |     // ## Path to client key file
 | ||||||
|  |     // ##
 | ||||||
|  |     pub key_file: Option<String>, | ||||||
|  | 
 | ||||||
|  |     // ## Server name used to verify host name
 | ||||||
|  |     // ##
 | ||||||
|  |     pub server_name: Option<String>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct ServiceMonitorEndpoint { | ||||||
|  |     // ## Name of the endpoint's service port
 | ||||||
|  |     // ## Mutually exclusive with targetPort
 | ||||||
|  |     pub port: Option<String>, | ||||||
|  | 
 | ||||||
|  |     // ## Name or number of the endpoint's target port
 | ||||||
|  |     // ## Mutually exclusive with port
 | ||||||
|  |     pub target_port: Option<String>, | ||||||
|  | 
 | ||||||
|  |     // ## File containing bearer token to be used when scraping targets
 | ||||||
|  |     // ##
 | ||||||
|  |     pub bearer_token_file: Option<String>, | ||||||
|  | 
 | ||||||
|  |     // ## Interval at which metrics should be scraped
 | ||||||
|  |     // ##
 | ||||||
|  |     pub interval: Option<String>, | ||||||
|  | 
 | ||||||
|  |     // ## HTTP path to scrape for metrics
 | ||||||
|  |     // ##
 | ||||||
|  |     pub path: String, | ||||||
|  | 
 | ||||||
|  |     // ## HTTP scheme to use for scraping
 | ||||||
|  |     // ##
 | ||||||
|  |     pub scheme: HTTPScheme, | ||||||
|  | 
 | ||||||
|  |     // ## TLS configuration to use when scraping the endpoint
 | ||||||
|  |     // ##
 | ||||||
|  |     pub tls_config: Option<ServiceMonitorTLSConfig>, | ||||||
|  | 
 | ||||||
|  |     // ## MetricRelabelConfigs to apply to samples after scraping, but before ingestion.
 | ||||||
|  |     // ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api-reference/api.md#relabelconfig
 | ||||||
|  |     // ##
 | ||||||
|  |     // # - action: keep
 | ||||||
|  |     // #   regex: 'kube_(daemonset|deployment|pod|namespace|node|statefulset).+'
 | ||||||
|  |     // #   sourceLabels: [__name__]
 | ||||||
|  |     pub metric_relabelings: Vec<Mapping>, | ||||||
|  | 
 | ||||||
|  |     // ## RelabelConfigs to apply to samples before scraping
 | ||||||
|  |     // ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api-reference/api.md#relabelconfig
 | ||||||
|  |     // ##
 | ||||||
|  |     // # - sourceLabels: [__meta_kubernetes_pod_node_name]
 | ||||||
|  |     // #   separator: ;
 | ||||||
|  |     // #   regex: ^(.*)$
 | ||||||
|  |     // #   targetLabel: nodename
 | ||||||
|  |     // #   replacement: $1
 | ||||||
|  |     // #   action: replace
 | ||||||
|  |     pub relabelings: Vec<Mapping>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct MatchExpression { | ||||||
|  |     pub key: String, | ||||||
|  |     pub operator: Operator, | ||||||
|  |     pub values: Vec<String>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct Selector { | ||||||
|  |     //   # label selector for services
 | ||||||
|  |     pub match_labels: HashMap<String, String>, | ||||||
|  |     pub match_expressions: Vec<MatchExpression>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | pub struct ServiceMonitor { | ||||||
|  |     pub name: String, | ||||||
|  | 
 | ||||||
|  |     // # Additional labels to set used for the ServiceMonitorSelector. Together with standard labels from the chart
 | ||||||
|  |     pub additional_labels: Option<Mapping>, | ||||||
|  | 
 | ||||||
|  |     // # Service label for use in assembling a job name of the form <label value>-<port>
 | ||||||
|  |     // # If no label is specified, the service name is used.
 | ||||||
|  |     pub job_label: Option<String>, | ||||||
|  | 
 | ||||||
|  |     // # labels to transfer from the kubernetes service to the target
 | ||||||
|  |     pub target_labels: Vec<String>, | ||||||
|  | 
 | ||||||
|  |     // # labels to transfer from the kubernetes pods to the target
 | ||||||
|  |     pub pod_target_labels: Vec<String>, | ||||||
|  | 
 | ||||||
|  |     // # Label selector for services to which this ServiceMonitor applies
 | ||||||
|  |     //   # Example which selects all services to be monitored
 | ||||||
|  |     //   # with label "monitoredby" with values any of "example-service-1" or "example-service-2"
 | ||||||
|  |     //   matchExpressions:
 | ||||||
|  |     //     - key: "monitoredby"
 | ||||||
|  |     //       operator: In
 | ||||||
|  |     //       values:
 | ||||||
|  |     //         - example-service-1
 | ||||||
|  |     //         - example-service-2
 | ||||||
|  |     pub selector: Selector, | ||||||
|  | 
 | ||||||
|  |     // # Namespaces from which services are selected
 | ||||||
|  |     //   # Match any namespace
 | ||||||
|  |     //   any: bool,
 | ||||||
|  |     //   # Explicit list of namespace names to select
 | ||||||
|  |     //   matchNames: Vec,
 | ||||||
|  |     pub namespace_selector: Option<Mapping>, | ||||||
|  | 
 | ||||||
|  |     // # Endpoints of the selected service to be monitored
 | ||||||
|  |     pub endpoints: Vec<ServiceMonitorEndpoint>, | ||||||
|  | 
 | ||||||
|  |     // # Fallback scrape protocol used by Prometheus for scraping metrics
 | ||||||
|  |     // # ref: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api-reference/api.md#monitoring.coreos.com/v1.ScrapeProtocol
 | ||||||
|  |     pub fallback_scrape_protocol: Option<String>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Default for ServiceMonitor { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             name: Default::default(), | ||||||
|  |             additional_labels: Default::default(), | ||||||
|  |             job_label: Default::default(), | ||||||
|  |             target_labels: Default::default(), | ||||||
|  |             pod_target_labels: Default::default(), | ||||||
|  |             selector: Selector { | ||||||
|  |                 match_labels: HashMap::new(), | ||||||
|  |                 match_expressions: vec![], | ||||||
|  |             }, | ||||||
|  |             namespace_selector: Default::default(), | ||||||
|  |             endpoints: Default::default(), | ||||||
|  |             fallback_scrape_protocol: Default::default(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Default for ServiceMonitorEndpoint { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             port: Some("80".to_string()), | ||||||
|  |             target_port: Default::default(), | ||||||
|  |             bearer_token_file: Default::default(), | ||||||
|  |             interval: Default::default(), | ||||||
|  |             path: "/metrics".to_string(), | ||||||
|  |             scheme: HTTPScheme::HTTP, | ||||||
|  |             tls_config: Default::default(), | ||||||
|  |             metric_relabelings: Default::default(), | ||||||
|  |             relabelings: Default::default(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
| pub mod alert_channel; | pub mod alert_channel; | ||||||
| pub mod alert_rule; | pub mod alert_rule; | ||||||
| pub mod kube_prometheus; | pub mod kube_prometheus; | ||||||
|  | pub mod ntfy; | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								harmony/src/modules/monitoring/ntfy/helm/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								harmony/src/modules/monitoring/ntfy/helm/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | pub mod ntfy_helm_chart; | ||||||
							
								
								
									
										83
									
								
								harmony/src/modules/monitoring/ntfy/helm/ntfy_helm_chart.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								harmony/src/modules/monitoring/ntfy/helm/ntfy_helm_chart.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | |||||||
|  | use non_blank_string_rs::NonBlankString; | ||||||
|  | use std::str::FromStr; | ||||||
|  | 
 | ||||||
|  | use crate::modules::helm::chart::{HelmChartScore, HelmRepository}; | ||||||
|  | 
 | ||||||
|  | pub fn ntfy_helm_chart_score(namespace: String) -> HelmChartScore { | ||||||
|  |     let values = format!( | ||||||
|  |         r#" | ||||||
|  | replicaCount: 1 | ||||||
|  | 
 | ||||||
|  | image: | ||||||
|  |   repository: binwiederhier/ntfy | ||||||
|  |   pullPolicy: IfNotPresent | ||||||
|  |   # Overrides the image tag whose default is the chart appVersion. | ||||||
|  |   tag: "v2.12.0" | ||||||
|  | 
 | ||||||
|  | serviceAccount: | ||||||
|  |   # Specifies whether a service account should be created | ||||||
|  |   create: true | ||||||
|  |   # Annotations to add to the service account | ||||||
|  |   # annotations: | ||||||
|  |   # The name of the service account to use. | ||||||
|  |   # If not set and create is true, a name is generated using the fullname template | ||||||
|  |   # name: "" | ||||||
|  | 
 | ||||||
|  | service: | ||||||
|  |   type: ClusterIP | ||||||
|  |   port: 80 | ||||||
|  | 
 | ||||||
|  | ingress: | ||||||
|  |   enabled: false | ||||||
|  | #  annotations: | ||||||
|  |     # kubernetes.io/ingress.class: nginx | ||||||
|  |     # kubernetes.io/tls-acme: "true" | ||||||
|  |   hosts: | ||||||
|  |     - host: ntfy.host.com | ||||||
|  |       paths: | ||||||
|  |         - path: / | ||||||
|  |           pathType: ImplementationSpecific | ||||||
|  |   tls: [] | ||||||
|  |   #  - secretName: chart-example-tls | ||||||
|  |   #    hosts: | ||||||
|  |   #      - chart-example.local | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | autoscaling: | ||||||
|  |   enabled: false | ||||||
|  | 
 | ||||||
|  | config: | ||||||
|  |   enabled: true | ||||||
|  |   data: | ||||||
|  | #    base-url: "https://ntfy.something.com" | ||||||
|  |     auth-file: "/var/cache/ntfy/user.db" | ||||||
|  |     auth-default-access: "deny-all" | ||||||
|  |     cache-file: "/var/cache/ntfy/cache.db" | ||||||
|  |     attachment-cache-dir: "/var/cache/ntfy/attachments" | ||||||
|  |     behind-proxy: true | ||||||
|  |     # web-root: "disable" | ||||||
|  |     enable-signup: false | ||||||
|  |     enable-login: "true" | ||||||
|  | 
 | ||||||
|  | persistence: | ||||||
|  |   enabled: true | ||||||
|  |   size: 200Mi | ||||||
|  | "#,
 | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     HelmChartScore { | ||||||
|  |         namespace: Some(NonBlankString::from_str(&namespace).unwrap()), | ||||||
|  |         release_name: NonBlankString::from_str("ntfy").unwrap(), | ||||||
|  |         chart_name: NonBlankString::from_str("sarab97/ntfy").unwrap(), | ||||||
|  |         chart_version: Some(NonBlankString::from_str("0.1.7").unwrap()), | ||||||
|  |         values_overrides: None, | ||||||
|  |         values_yaml: Some(values.to_string()), | ||||||
|  |         create_namespace: true, | ||||||
|  |         install_only: false, | ||||||
|  |         repository: Some(HelmRepository::new( | ||||||
|  |             "sarab97".to_string(), | ||||||
|  |             url::Url::parse("https://charts.sarabsingh.com").unwrap(), | ||||||
|  |             true, | ||||||
|  |         )), | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								harmony/src/modules/monitoring/ntfy/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								harmony/src/modules/monitoring/ntfy/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | pub mod helm; | ||||||
|  | pub mod ntfy; | ||||||
							
								
								
									
										169
									
								
								harmony/src/modules/monitoring/ntfy/ntfy.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								harmony/src/modules/monitoring/ntfy/ntfy.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,169 @@ | |||||||
|  | use std::sync::Arc; | ||||||
|  | 
 | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use log::debug; | ||||||
|  | use serde::Serialize; | ||||||
|  | use strum::{Display, EnumString}; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     data::{Id, Version}, | ||||||
|  |     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||||
|  |     inventory::Inventory, | ||||||
|  |     modules::monitoring::ntfy::helm::ntfy_helm_chart::ntfy_helm_chart_score, | ||||||
|  |     score::Score, | ||||||
|  |     topology::{HelmCommand, K8sclient, Topology, k8s::K8sClient}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, Serialize)] | ||||||
|  | pub struct NtfyScore { | ||||||
|  |     pub namespace: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<T: Topology + HelmCommand + K8sclient> Score<T> for NtfyScore { | ||||||
|  |     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||||
|  |         Box::new(NtfyInterpret { | ||||||
|  |             score: self.clone(), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn name(&self) -> String { | ||||||
|  |         format!("Ntfy") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Serialize)] | ||||||
|  | pub struct NtfyInterpret { | ||||||
|  |     pub score: NtfyScore, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, EnumString, Display)] | ||||||
|  | enum NtfyAccessMode { | ||||||
|  |     #[strum(serialize = "read-write", serialize = "rw", to_string = "read-write")] | ||||||
|  |     ReadWrite, | ||||||
|  |     #[strum(
 | ||||||
|  |         serialize = "read-only", | ||||||
|  |         serialize = "ro", | ||||||
|  |         serialize = "read", | ||||||
|  |         to_string = "read-only" | ||||||
|  |     )] | ||||||
|  |     ReadOnly, | ||||||
|  |     #[strum(
 | ||||||
|  |         serialize = "write-only", | ||||||
|  |         serialize = "wo", | ||||||
|  |         serialize = "write", | ||||||
|  |         to_string = "write-only" | ||||||
|  |     )] | ||||||
|  |     WriteOnly, | ||||||
|  |     #[strum(serialize = "none", to_string = "deny")] | ||||||
|  |     Deny, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, EnumString, Display)] | ||||||
|  | enum NtfyRole { | ||||||
|  |     #[strum(serialize = "user", to_string = "user")] | ||||||
|  |     User, | ||||||
|  |     #[strum(serialize = "admin", to_string = "admin")] | ||||||
|  |     Admin, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl NtfyInterpret { | ||||||
|  |     async fn add_user( | ||||||
|  |         &self, | ||||||
|  |         k8s_client: Arc<K8sClient>, | ||||||
|  |         username: &str, | ||||||
|  |         password: &str, | ||||||
|  |         role: Option<NtfyRole>, | ||||||
|  |     ) -> Result<(), String> { | ||||||
|  |         let role = match role { | ||||||
|  |             Some(r) => r, | ||||||
|  |             None => NtfyRole::User, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         k8s_client | ||||||
|  |             .exec_app( | ||||||
|  |                 "ntfy".to_string(), | ||||||
|  |                 Some(&self.score.namespace), | ||||||
|  |                 vec![ | ||||||
|  |                     "sh", | ||||||
|  |                     "-c", | ||||||
|  |                     format!("NTFY_PASSWORD={password} ntfy user add --role={role} {username}") | ||||||
|  |                         .as_str(), | ||||||
|  |                 ], | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn set_access( | ||||||
|  |         &self, | ||||||
|  |         k8s_client: Arc<K8sClient>, | ||||||
|  |         username: &str, | ||||||
|  |         topic: &str, | ||||||
|  |         mode: NtfyAccessMode, | ||||||
|  |     ) -> Result<(), String> { | ||||||
|  |         k8s_client | ||||||
|  |             .exec_app( | ||||||
|  |                 "ntfy".to_string(), | ||||||
|  |                 Some(&self.score.namespace), | ||||||
|  |                 vec![ | ||||||
|  |                     "sh", | ||||||
|  |                     "-c", | ||||||
|  |                     format!("ntfy access {username} {topic} {mode}").as_str(), | ||||||
|  |                 ], | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// We need a ntfy interpret to wrap the HelmChartScore in order to run the score, and then bootstrap the config inside ntfy
 | ||||||
|  | #[async_trait] | ||||||
|  | impl<T: Topology + HelmCommand + K8sclient> Interpret<T> for NtfyInterpret { | ||||||
|  |     async fn execute( | ||||||
|  |         &self, | ||||||
|  |         inventory: &Inventory, | ||||||
|  |         topology: &T, | ||||||
|  |     ) -> Result<Outcome, InterpretError> { | ||||||
|  |         ntfy_helm_chart_score(self.score.namespace.clone()) | ||||||
|  |             .create_interpret() | ||||||
|  |             .execute(inventory, topology) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         debug!("installed ntfy helm chart"); | ||||||
|  |         let client = topology | ||||||
|  |             .k8s_client() | ||||||
|  |             .await | ||||||
|  |             .expect("couldn't get k8s client"); | ||||||
|  | 
 | ||||||
|  |         client | ||||||
|  |             .wait_until_deployment_ready( | ||||||
|  |                 "ntfy".to_string(), | ||||||
|  |                 Some(&self.score.namespace.as_str()), | ||||||
|  |                 None, | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         debug!("created k8s client"); | ||||||
|  | 
 | ||||||
|  |         self.add_user(client, "harmony", "harmony", Some(NtfyRole::Admin)) | ||||||
|  |             .await?; | ||||||
|  | 
 | ||||||
|  |         debug!("exec into pod done"); | ||||||
|  | 
 | ||||||
|  |         Ok(Outcome::success("installed ntfy".to_string())) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn get_name(&self) -> InterpretName { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  |     fn get_version(&self) -> Version { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  |     fn get_status(&self) -> InterpretStatus { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  |     fn get_children(&self) -> Vec<Id> { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	
as noted in a comment below, should remove TenantManager later