Compare commits

..

3 Commits

48 changed files with 614 additions and 720 deletions

1
.gitignore vendored
View File

@@ -2,4 +2,3 @@ target
private_repos private_repos
log/ log/
*.tgz *.tgz
.gitignore

153
Cargo.lock generated
View File

@@ -219,15 +219,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "atomic"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -418,12 +409,6 @@ version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytemuck"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@@ -1358,7 +1343,6 @@ dependencies = [
"cidr", "cidr",
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli",
"harmony_macros", "harmony_macros",
"harmony_tui", "harmony_tui",
"harmony_types", "harmony_types",
@@ -1371,7 +1355,6 @@ dependencies = [
name = "example-rust" name = "example-rust"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1",
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli", "harmony_cli",
@@ -1444,31 +1427,6 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "figment"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic",
"pear",
"serde",
"uncased",
"version_check",
]
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.2" version = "1.1.2"
@@ -1768,8 +1726,6 @@ name = "harmony"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.22.1",
"bollard",
"chrono", "chrono",
"cidr", "cidr",
"convert_case", "convert_case",
@@ -1779,7 +1735,6 @@ dependencies = [
"dyn-clone", "dyn-clone",
"email_address", "email_address",
"env_logger", "env_logger",
"figment",
"fqdn", "fqdn",
"futures-util", "futures-util",
"harmony_macros", "harmony_macros",
@@ -1797,7 +1752,6 @@ dependencies = [
"non-blank-string-rs", "non-blank-string-rs",
"opnsense-config", "opnsense-config",
"opnsense-config-xml", "opnsense-config-xml",
"pretty_assertions",
"rand 0.9.1", "rand 0.9.1",
"reqwest 0.11.27", "reqwest 0.11.27",
"russh", "russh",
@@ -1806,11 +1760,9 @@ dependencies = [
"serde", "serde",
"serde-value", "serde-value",
"serde_json", "serde_json",
"serde_with",
"serde_yaml", "serde_yaml",
"similar", "similar",
"strum 0.27.1", "strum 0.27.1",
"tar",
"temp-dir", "temp-dir",
"temp-file", "temp-file",
"tempfile", "tempfile",
@@ -2445,12 +2397,6 @@ version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]] [[package]]
name = "inout" name = "inout"
version = "0.1.4" version = "0.1.4"
@@ -2780,7 +2726,6 @@ checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"libc", "libc",
"redox_syscall",
] ]
[[package]] [[package]]
@@ -3272,29 +3217,6 @@ dependencies = [
"hmac", "hmac",
] ]
[[package]]
name = "pear"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
dependencies = [
"inlinable_string",
"pear_codegen",
"yansi",
]
[[package]]
name = "pear_codegen"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
]
[[package]] [[package]]
name = "pem" name = "pem"
version = "3.0.5" version = "3.0.5"
@@ -3557,19 +3479,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
"yansi",
]
[[package]] [[package]]
name = "punycode" name = "punycode"
version = "0.4.1" version = "0.4.1"
@@ -4171,18 +4080,6 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "schemars"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -4383,36 +4280,22 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.14.0" version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.10.0", "indexmap 2.10.0",
"schemars 0.9.0", "schemars",
"schemars 1.0.3",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"serde_with_macros",
"time", "time",
] ]
[[package]]
name = "serde_with_macros"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "serde_yaml" name = "serde_yaml"
version = "0.9.34+deprecated" version = "0.9.34+deprecated"
@@ -4785,17 +4668,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "temp-dir" name = "temp-dir"
version = "0.1.16" version = "0.1.16"
@@ -5212,15 +5084,6 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@@ -5850,16 +5713,6 @@ dependencies = [
"tap", "tap",
] ]
[[package]]
name = "xattr"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
dependencies = [
"libc",
"rustix 1.0.7",
]
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.26" version = "0.8.26"

View File

@@ -52,8 +52,3 @@ 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"] }
pretty_assertions = "1.4.1"
bollard = "0.19.1"
base64 = "0.22.1"
tar = "0.4.44"
figment = { version = "0.10.19", features = ["env"] }

View File

@@ -10,7 +10,7 @@ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro, maestro::Maestro,
modules::{ modules::{
http::StaticFilesHttpScore, http::HttpScore,
ipxe::IpxeScore, ipxe::IpxeScore,
okd::{ okd::{
bootstrap_dhcp::OKDBootstrapDhcpScore, bootstrap_dhcp::OKDBootstrapDhcpScore,
@@ -126,7 +126,7 @@ async fn main() {
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
let http_score = StaticFilesHttpScore::new(Url::LocalFolder( let http_score = HttpScore::new(Url::LocalFolder(
"./data/watchguard/pxe-http-files".to_string(), "./data/watchguard/pxe-http-files".to_string(),
)); ));
let ipxe_score = IpxeScore::new(); let ipxe_score = IpxeScore::new();

View File

@@ -14,7 +14,6 @@ async fn main() {
maestro.register_all(vec![Box::new(NtfyScore { maestro.register_all(vec![Box::new(NtfyScore {
namespace: "monitoring".to_string(), namespace: "monitoring".to_string(),
host: "localhost".to_string(),
})]); })]);
harmony_cli::init(maestro, None).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@@ -16,4 +16,3 @@ 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 }
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }

View File

@@ -11,9 +11,9 @@ use harmony::{
maestro::Maestro, maestro::Maestro,
modules::{ modules::{
dummy::{ErrorScore, PanicScore, SuccessScore}, dummy::{ErrorScore, PanicScore, SuccessScore},
http::StaticFilesHttpScore, http::HttpScore,
okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore}, okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore},
opnsense::{OPNSenseLaunchUpgrade, OPNsenseShellCommandScore}, opnsense::OPNsenseShellCommandScore,
tftp::TftpScore, tftp::TftpScore,
}, },
topology::{LogicalHost, UnmanagedRouter, Url}, topology::{LogicalHost, UnmanagedRouter, Url},
@@ -22,10 +22,8 @@ use harmony_macros::{ip, mac_address};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init();
let firewall = harmony::topology::LogicalHost { let firewall = harmony::topology::LogicalHost {
ip: ip!("192.168.122.106"), ip: ip!("192.168.5.229"),
name: String::from("opnsense-1"), name: String::from("opnsense-1"),
}; };
@@ -83,7 +81,7 @@ async fn main() {
let load_balancer_score = OKDLoadBalancerScore::new(&topology); let load_balancer_score = OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
let http_score = StaticFilesHttpScore::new(Url::LocalFolder( let http_score = HttpScore::new(Url::LocalFolder(
"./data/watchguard/pxe-http-files".to_string(), "./data/watchguard/pxe-http-files".to_string(),
)); ));
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
@@ -97,12 +95,9 @@ async fn main() {
opnsense: opnsense.get_opnsense_config(), opnsense: opnsense.get_opnsense_config(),
command: "touch /tmp/helloharmonytouching".to_string(), command: "touch /tmp/helloharmonytouching".to_string(),
}), }),
// Box::new(OPNSenseLaunchUpgrade {
// opnsense: opnsense.get_opnsense_config(),
// }),
Box::new(SuccessScore {}), Box::new(SuccessScore {}),
Box::new(ErrorScore {}), Box::new(ErrorScore {}),
Box::new(PanicScore {}), Box::new(PanicScore {}),
]); ]);
harmony_cli::init(maestro, None).await.unwrap(); harmony_tui::init(maestro).await.unwrap();
} }

View File

@@ -12,4 +12,3 @@ tokio = { workspace = true }
log = { workspace = true } log = { workspace = true }
env_logger = { workspace = true } env_logger = { workspace = true }
url = { workspace = true } url = { workspace = true }
base64.workspace = true

View File

@@ -3,9 +3,18 @@ use std::{path::PathBuf, sync::Arc};
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro, maestro::Maestro,
modules::application::{ modules::{
ApplicationScore, RustWebFramework, RustWebapp, application::{
features::{ContinuousDelivery, Monitoring}, ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, PrometheusMonitoring},
},
monitoring::{
alert_channel::discord_alert_channel::DiscordWebhook,
alert_rule::prometheus_alert_rule::AlertManagerRuleGroup,
},
prometheus::alerts::k8s::{
pod::pod_in_failed_state, pvc::high_pvc_fill_rate_over_two_days,
},
}, },
topology::{K8sAnywhereTopology, Url}, topology::{K8sAnywhereTopology, Url},
}; };
@@ -13,12 +22,6 @@ use harmony::{
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); env_logger::init();
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
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: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
@@ -26,18 +29,34 @@ async fn main() {
framework: Some(RustWebFramework::Leptos), framework: Some(RustWebFramework::Leptos),
}); });
let pod_failed = pod_in_failed_state();
let pod_failed_2 = pod_in_failed_state();
let pod_failed_3 = pod_in_failed_state();
let additional_rules = AlertManagerRuleGroup::new("pod-alerts", vec![pod_failed]);
let additional_rules_2 = AlertManagerRuleGroup::new("pod-alerts-2", vec![pod_failed_2, pod_failed_3]);
let app = ApplicationScore { let app = ApplicationScore {
features: vec![ features: vec![
Box::new(ContinuousDelivery { //Box::new(ContinuousDelivery {
application: application.clone(), // application: application.clone(),
}), // TODO add monitoring, backups, multisite ha, etc //}),
Box::new(Monitoring { Box::new(PrometheusMonitoring {
application: application.clone(), application: application.clone(),
alert_receivers: vec![Box::new(DiscordWebhook {
name: "dummy-discord".to_string(),
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
})],
alert_rules: vec![Box::new(additional_rules), Box::new(additional_rules_2)],
}), }),
// TODO add monitoring, backups, multisite ha, etc
], ],
application, application,
}; };
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
maestro.register_all(vec![Box::new(app)]); maestro.register_all(vec![Box::new(app)]);
harmony_cli::init(maestro, None).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@@ -58,11 +58,3 @@ futures-util = "0.3.31"
tokio-util = "0.7.15" tokio-util = "0.7.15"
strum = { version = "0.27.1", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
tempfile = "3.20.0" tempfile = "3.20.0"
serde_with = "3.14.0"
bollard.workspace = true
tar.workspace = true
base64.workspace = true
figment.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View File

@@ -1,66 +1,15 @@
use figment::{
Error, Figment, Metadata, Profile, Provider,
providers::{Env, Format},
value::{Dict, Map},
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize)] lazy_static! {
pub struct Config { pub static ref HARMONY_DATA_DIR: PathBuf = directories::BaseDirs::new()
pub data_dir: PathBuf, .unwrap()
pub registry_url: String, .data_dir()
pub registry_project: String, .join("harmony");
pub dry_run: bool, pub static ref REGISTRY_URL: String =
pub run_upgrades: bool, std::env::var("HARMONY_REGISTRY_URL").unwrap_or_else(|_| "hub.nationtech.io".to_string());
} pub static ref REGISTRY_PROJECT: String =
std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string());
impl Default for Config { pub static ref DRY_RUN: bool =
fn default() -> Self { std::env::var("HARMONY_DRY_RUN").map_or(true, |value| value.parse().unwrap_or(true));
Config {
data_dir: directories::BaseDirs::new()
.unwrap()
.data_dir()
.join("harmony"),
registry_url: "hub.nationtech.io".to_string(),
registry_project: "harmony".to_string(),
dry_run: true,
run_upgrades: false,
}
}
}
impl Config {
pub fn load() -> Result<Self, figment::Error> {
Figment::from(Config::default())
.merge(Env::prefixed("HARMONY_"))
.extract()
}
fn from<T: Provider>(provider: T) -> Result<Config, Error> {
Figment::from(provider).extract()
}
fn figment() -> Figment {
use figment::providers::Env;
// In reality, whatever the library desires.
Figment::from(Config::default()).merge(Env::prefixed("HARMONY_"))
}
}
impl Provider for Config {
fn metadata(&self) -> Metadata {
Metadata::named("Harmony Config")
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
figment::providers::Serialized::defaults(Config::default()).data()
}
fn profile(&self) -> Option<Profile> {
// Optionally, a profile that's selected by default.
Some(Profile::Default)
}
} }

View File

@@ -1,15 +1,11 @@
use async_trait::async_trait; use async_trait::async_trait;
use harmony_macros::ip; use harmony_macros::ip;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::error;
use log::info; use log::info;
use crate::config::Config;
use crate::executors::ExecutorError; use crate::executors::ExecutorError;
use crate::interpret::InterpretError; use crate::interpret::InterpretError;
use crate::interpret::Outcome; use crate::interpret::Outcome;
use crate::inventory::Inventory;
use crate::topology::upgradeable::Upgradeable;
use super::DHCPStaticEntry; use super::DHCPStaticEntry;
use super::DhcpServer; use super::DhcpServer;
@@ -29,12 +25,9 @@ use super::TftpServer;
use super::Topology; use super::Topology;
use super::Url; use super::Url;
use super::k8s::K8sClient; use super::k8s::K8sClient;
use std::fmt::Debug;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone, Debug)] #[derive(Debug, Clone)]
pub struct HAClusterTopology { pub struct HAClusterTopology {
pub domain_name: String, pub domain_name: String,
pub router: Arc<dyn Router>, pub router: Arc<dyn Router>,
@@ -56,15 +49,9 @@ impl Topology for HAClusterTopology {
"HAClusterTopology" "HAClusterTopology"
} }
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> { async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
error!( todo!(
"ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready." "ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready."
); )
let config = Config::load().expect("couldn't load config");
if config.run_upgrades {
self.upgrade(&Inventory::empty(), self).await?;
}
Ok(Outcome::success("for now do nothing".to_string()))
} }
} }
@@ -264,13 +251,6 @@ impl Topology for DummyInfra {
} }
} }
#[async_trait]
impl<T: Topology> Upgradeable<T> for DummyInfra {
async fn upgrade(&self, _inventory: &Inventory, _topology: &T) -> Result<(), InterpretError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
}
const UNIMPLEMENTED_DUMMY_INFRA: &str = "This is a dummy infrastructure, no operation is supported"; const UNIMPLEMENTED_DUMMY_INFRA: &str = "This is a dummy infrastructure, no operation is supported";
impl Router for DummyInfra { impl Router for DummyInfra {
@@ -437,12 +417,3 @@ impl DnsServer for DummyInfra {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }
} }
#[async_trait]
impl<T: Topology> Upgradeable<T> for HAClusterTopology {
async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
error!("TODO implement upgrades for all parts of the cluster");
self.firewall.upgrade(inventory, topology).await?;
Ok(())
}
}

View File

@@ -20,8 +20,6 @@ use log::{debug, error, trace};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use similar::{DiffableStr, TextDiff}; use similar::{DiffableStr, TextDiff};
use crate::config::Config as HarmonyConfig;
#[derive(new, Clone)] #[derive(new, Clone)]
pub struct K8sClient { pub struct K8sClient {
client: Client, client: Client,
@@ -156,9 +154,7 @@ impl K8sClient {
.as_ref() .as_ref()
.expect("K8s Resource should have a name"); .expect("K8s Resource should have a name");
let config = HarmonyConfig::load().expect("couldn't load config"); if *crate::config::DRY_RUN {
if config.dry_run {
match api.get(name).await { match api.get(name).await {
Ok(current) => { Ok(current) => {
trace!("Received current value {current:#?}"); trace!("Received current value {current:#?}");
@@ -264,33 +260,17 @@ impl K8sClient {
) -> Result<(), Error> { ) -> Result<(), Error> {
let obj: DynamicObject = serde_yaml::from_value(yaml.clone()).expect("TODO do not unwrap"); let obj: DynamicObject = serde_yaml::from_value(yaml.clone()).expect("TODO do not unwrap");
let name = obj.metadata.name.as_ref().expect("YAML must have a name"); let name = obj.metadata.name.as_ref().expect("YAML must have a name");
let namespace = obj
.metadata
.namespace
.as_ref()
.expect("YAML must have a namespace");
let api_version = yaml // 4. Define the API resource type using the GVK from the object.
.get("apiVersion") // The plural name 'applications' is taken from your CRD definition.
.expect("couldn't get apiVersion from YAML") error!("This only supports argocd application harcoded, very rrrong");
.as_str() let gvk = GroupVersionKind::gvk("argoproj.io", "v1alpha1", "Application");
.expect("couldn't get apiVersion as str"); let api_resource = ApiResource::from_gvk_with_plural(&gvk, "applications");
let kind = yaml
.get("kind")
.expect("couldn't get kind from YAML")
.as_str()
.expect("couldn't get kind as str");
let split: Vec<&str> = api_version.splitn(2, "/").collect();
let g = split[0];
let v = split[1];
let gvk = GroupVersionKind::gvk(g, v, kind);
let api_resource = ApiResource::from_gvk(&gvk);
let namespace = match ns {
Some(n) => n,
None => obj
.metadata
.namespace
.as_ref()
.expect("YAML must have a namespace"),
};
// 5. Create a dynamic API client for this resource type. // 5. Create a dynamic API client for this resource type.
let api: Api<DynamicObject> = let api: Api<DynamicObject> =

View File

@@ -1,10 +1,9 @@
use std::{process::Command, sync::Arc}; use std::{process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use figment::{Figment, providers::Env};
use inquire::Confirm; use inquire::Confirm;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::{Deserialize, Serialize}; use serde::Serialize;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use crate::{ use crate::{
@@ -220,7 +219,7 @@ impl K8sAnywhereTopology {
} }
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug)]
pub struct K8sAnywhereConfig { pub struct K8sAnywhereConfig {
/// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes
/// cluster /// cluster
@@ -247,29 +246,25 @@ pub struct K8sAnywhereConfig {
/// ///
/// default: true /// default: true
pub use_local_k3d: bool, pub use_local_k3d: bool,
pub profile: String, pub harmony_profile: String,
}
impl Default for K8sAnywhereConfig {
fn default() -> Self {
Self {
kubeconfig: None,
use_system_kubeconfig: false,
autoinstall: false,
// TODO harmony_profile should be managed at a more core level than this
profile: "dev".to_string(),
use_local_k3d: true,
}
}
} }
impl K8sAnywhereConfig { impl K8sAnywhereConfig {
fn from_env() -> Self { fn from_env() -> Self {
Figment::new() Self {
.merge(Env::prefixed("HARMONY_")) kubeconfig: std::env::var("KUBECONFIG").ok().map(|v| v.to_string()),
.merge(Env::raw().only(&["KUBECONFIG"])) use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG")
.extract() .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
.expect("couldn't load config from env") autoinstall: std::env::var("HARMONY_AUTOINSTALL")
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
// TODO harmony_profile should be managed at a more core level than this
harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else(
|_| "dev".to_string(),
|v| v.parse().ok().unwrap_or("dev".to_string()),
),
use_local_k3d: std::env::var("HARMONY_USE_LOCAL_K3D")
.map_or_else(|_| true, |v| v.parse().ok().unwrap_or(true)),
}
} }
} }
@@ -309,7 +304,7 @@ impl MultiTargetTopology for K8sAnywhereTopology {
return DeploymentTarget::LocalDev; return DeploymentTarget::LocalDev;
} }
match self.config.profile.to_lowercase().as_str() { match self.config.harmony_profile.to_lowercase().as_str() {
"staging" => DeploymentTarget::Staging, "staging" => DeploymentTarget::Staging,
"production" => DeploymentTarget::Production, "production" => DeploymentTarget::Production,
_ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"), _ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"),

View File

@@ -6,7 +6,6 @@ mod k8s_anywhere;
mod localhost; mod localhost;
pub mod oberservability; pub mod oberservability;
pub mod tenant; pub mod tenant;
pub mod upgradeable;
pub use k8s_anywhere::*; pub use k8s_anywhere::*;
pub use localhost::*; pub use localhost::*;
pub mod k8s; pub mod k8s;

View File

@@ -2,15 +2,9 @@ use std::{net::Ipv4Addr, str::FromStr, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::debug;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::executors::ExecutorError;
executors::ExecutorError,
interpret::InterpretError,
inventory::Inventory,
topology::{Topology, upgradeable::Upgradeable},
};
use super::{IpAddress, LogicalHost, k8s::K8sClient}; use super::{IpAddress, LogicalHost, k8s::K8sClient};
@@ -44,15 +38,6 @@ impl std::fmt::Debug for dyn Firewall {
} }
} }
// #[async_trait]
// impl<T: Topology> Upgradeable<T> for dyn Firewall {
// async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
// debug!("upgrading");
// self.upgrade(inventory, topology).await?;
// Ok(())
// }
// }
pub struct NetworkDomain { pub struct NetworkDomain {
pub name: String, pub name: String,
} }

View File

@@ -1,3 +1,5 @@
use std::any::Any;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
@@ -9,7 +11,7 @@ use crate::{
}; };
#[async_trait] #[async_trait]
pub trait AlertSender: Send + Sync + std::fmt::Debug { pub trait AlertSender: Any + Send + Sync + std::fmt::Debug {
fn name(&self) -> String; fn name(&self) -> String;
} }

View File

@@ -1,8 +0,0 @@
use async_trait::async_trait;
use crate::{interpret::InterpretError, inventory::Inventory};
#[async_trait]
pub trait Upgradeable<T>: Send + Sync {
async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError>;
}

View File

@@ -7,18 +7,13 @@ mod management;
mod tftp; mod tftp;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
pub use management::*; pub use management::*;
use opnsense_config_xml::Host; use opnsense_config_xml::Host;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{ use crate::{
executors::ExecutorError, executors::ExecutorError,
interpret::InterpretError, topology::{IpAddress, LogicalHost},
inventory::Inventory,
modules::opnsense::OPNSenseLaunchUpgrade,
score::Score,
topology::{IpAddress, LogicalHost, Topology, upgradeable::Upgradeable},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -54,17 +49,3 @@ impl OPNSenseFirewall {
.map_err(|e| ExecutorError::UnexpectedError(e.to_string())) .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))
} }
} }
#[async_trait]
impl<T: Topology> Upgradeable<T> for OPNSenseFirewall {
async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
OPNSenseLaunchUpgrade {
opnsense: self.get_opnsense_config(),
}
.create_interpret()
.execute(inventory, topology)
.await?;
Ok(())
}
}

View File

@@ -1,11 +1,13 @@
use std::{backtrace, collections::HashMap};
use k8s_openapi::{Metadata, NamespaceResourceScope, Resource};
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use serde_with::skip_serializing_none;
use serde_yaml::Value; use serde_yaml::Value;
use url::Url;
use crate::modules::application::features::CDApplicationConfig; use crate::modules::application::features::CDApplicationConfig;
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Helm { pub struct Helm {
@@ -25,18 +27,13 @@ pub struct Helm {
pub namespace: Option<String>, pub namespace: Option<String>,
} }
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Source { pub struct Source {
// Using string for this because URL enforces a URL scheme at the beginning but Helm, ArgoCD, etc do not, and it can be counterproductive, pub repo_url: Url,
// as the only way I've found to get OCI working isn't by using oci:// but rather no scheme at all
#[serde(rename = "repoURL")]
pub repo_url: String,
pub target_revision: Option<String>, pub target_revision: Option<String>,
pub chart: String, pub chart: String,
pub helm: Helm, pub helm: Helm,
pub path: String,
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@@ -70,7 +67,6 @@ pub struct SyncPolicy {
pub retry: Retry, pub retry: Retry,
} }
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ArgoApplication { pub struct ArgoApplication {
@@ -89,7 +85,7 @@ impl Default for ArgoApplication {
namespace: Default::default(), namespace: Default::default(),
project: Default::default(), project: Default::default(),
source: Source { source: Source {
repo_url: "http://asdf".to_string(), repo_url: Url::parse("http://asdf").expect("Couldn't parse to URL"),
target_revision: None, target_revision: None,
chart: "".to_string(), chart: "".to_string(),
helm: Helm { helm: Helm {
@@ -108,7 +104,6 @@ impl Default for ArgoApplication {
api_versions: vec![], api_versions: vec![],
namespace: None, namespace: None,
}, },
path: "".to_string(),
}, },
sync_policy: SyncPolicy { sync_policy: SyncPolicy {
automated: Automated { automated: Automated {
@@ -138,10 +133,10 @@ impl From<CDApplicationConfig> for ArgoApplication {
namespace: Some(value.namespace), namespace: Some(value.namespace),
project: "default".to_string(), project: "default".to_string(),
source: Source { source: Source {
repo_url: value.helm_chart_repo_url, repo_url: Url::parse(value.helm_chart_repo_url.to_string().as_str())
target_revision: Some(value.version.to_string()), .expect("couldn't convert to URL"),
chart: value.helm_chart_name.clone(), target_revision: None,
path: value.helm_chart_name, chart: value.helm_chart_name,
helm: Helm { helm: Helm {
pass_credentials: None, pass_credentials: None,
parameters: vec![], parameters: vec![],
@@ -150,7 +145,7 @@ impl From<CDApplicationConfig> for ArgoApplication {
value_files: vec![], value_files: vec![],
ignore_missing_value_files: None, ignore_missing_value_files: None,
values: None, values: None,
values_object: value.values_overrides, values_object: Some(value.values_overrides),
skip_crds: None, skip_crds: None,
skip_schema_validation: None, skip_schema_validation: None,
version: None, version: None,
@@ -218,7 +213,7 @@ spec:
let mut yaml_value: Value = let mut yaml_value: Value =
serde_yaml::from_str(yaml_str.as_str()).expect("couldn't parse string to YAML"); serde_yaml::from_str(yaml_str.as_str()).expect("couldn't parse string to YAML");
let spec = yaml_value let mut spec = yaml_value
.get_mut("spec") .get_mut("spec")
.expect("couldn't get spec from yaml") .expect("couldn't get spec from yaml")
.as_mapping_mut() .as_mapping_mut()
@@ -257,7 +252,7 @@ spec:
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use url::Url;
use crate::modules::application::features::{ use crate::modules::application::features::{
ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy, ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy,
@@ -270,7 +265,7 @@ mod tests {
namespace: Some("test-ns".to_string()), namespace: Some("test-ns".to_string()),
project: "test-project".to_string(), project: "test-project".to_string(),
source: Source { source: Source {
repo_url: "http://test".to_string(), repo_url: Url::parse("http://test").unwrap(),
target_revision: None, target_revision: None,
chart: "test-chart".to_string(), chart: "test-chart".to_string(),
helm: Helm { helm: Helm {
@@ -289,7 +284,6 @@ mod tests {
api_versions: vec![], api_versions: vec![],
namespace: None, namespace: None,
}, },
path: "".to_string(),
}, },
sync_policy: SyncPolicy { sync_policy: SyncPolicy {
automated: Automated { automated: Automated {
@@ -321,15 +315,24 @@ spec:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: test-ns namespace: test-ns
source: source:
repoURL: http://test repoUrl: http://test/
targetRevision: null
chart: test-chart chart: test-chart
helm: helm:
passCredentials: null
parameters: [] parameters: []
fileParameters: [] fileParameters: []
releaseName: test-release-neame releaseName: test-release-neame
valueFiles: [] valueFiles: []
ignoreMissingValueFiles: null
values: null
valuesObject: null
skipCrds: null
skipSchemaValidation: null
version: null
kubeVersion: null
apiVersions: [] apiVersions: []
path: '' namespace: null
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: false

View File

@@ -6,15 +6,18 @@ use serde_yaml::Value;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::{ use crate::{
config::Config, config::HARMONY_DATA_DIR,
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::application::{ modules::{
Application, ApplicationFeature, HelmPackage, OCICompliant, application::{
features::{ArgoApplication, ArgoHelmScore}, Application, ApplicationFeature, HelmPackage, OCICompliant,
features::{ArgoApplication, ArgoHelmScore},
},
helm::chart::HelmChartScore,
}, },
score::Score, score::Score,
topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology}, topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, Url},
}; };
/// ContinuousDelivery in Harmony provides this functionality : /// ContinuousDelivery in Harmony provides this functionality :
@@ -56,14 +59,12 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
chart_url: String, chart_url: String,
image_name: String, image_name: String,
) -> Result<(), String> { ) -> Result<(), String> {
let config = Config::load().expect("couldn't load config");
error!( error!(
"FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology" "FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology"
); );
error!("TODO hardcoded k3d bin path is wrong"); error!("TODO hardcoded k3d bin path is wrong");
let k3d_bin_path = config.data_dir.join("k3d").join("k3d"); let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d");
// --- 1. Import the container image into the k3d cluster --- // --- 1. Import the container image into the k3d cluster ---
info!( info!(
"Importing image '{}' into k3d cluster 'harmony'", "Importing image '{}' into k3d cluster 'harmony'",
@@ -160,7 +161,6 @@ impl<
let helm_chart = self.application.build_push_helm_package(&image).await?; let helm_chart = self.application.build_push_helm_package(&image).await?;
info!("Pushed new helm chart {helm_chart}"); info!("Pushed new helm chart {helm_chart}");
error!("TODO Make building image configurable/skippable");
let image = self.application.build_push_oci_image().await?; let image = self.application.build_push_oci_image().await?;
info!("Pushed new docker image {image}"); info!("Pushed new docker image {image}");
@@ -187,14 +187,14 @@ impl<
info!("Deploying to target {target:?}"); info!("Deploying to target {target:?}");
let score = ArgoHelmScore { let score = ArgoHelmScore {
namespace: "harmonydemo-staging".to_string(), namespace: "harmonydemo-staging".to_string(),
openshift: false, openshift: true,
domain: "argo.harmonydemo.apps.st.mcd".to_string(), domain: "argo.harmonydemo.apps.st.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/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: Url::Url(url::Url::parse("oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart/harmony-example-rust-webapp-chart").unwrap()),
helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), helm_chart_name: "harmony-example-rust-webapp-chart".to_string(),
values_overrides: None, values_overrides: Value::Null,
name: "harmony-demo-rust-webapp".to_string(), name: "harmony-demo-rust-webapp".to_string(),
namespace: "harmonydemo-staging".to_string(), namespace: "harmonydemo-staging".to_string(),
})], })],
@@ -206,7 +206,14 @@ impl<
.unwrap(); .unwrap();
} }
}; };
Ok(())
todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it
- [X] Package app (docker image, helm chart)
- [X] Push to registry
- [X] Push only if staging or prod
- [X] Deploy to local k3d when target is local
- [ ] Poke Argo
- [ ] Ensure app is up")
} }
fn name(&self) -> String { fn name(&self) -> String {
"ContinuousDelivery".to_string() "ContinuousDelivery".to_string()
@@ -217,9 +224,9 @@ impl<
/// more CD systems /// more CD systems
pub struct CDApplicationConfig { pub struct CDApplicationConfig {
pub version: Version, pub version: Version,
pub helm_chart_repo_url: String, pub helm_chart_repo_url: Url,
pub helm_chart_name: String, pub helm_chart_name: String,
pub values_overrides: Option<Value>, pub values_overrides: Value,
pub name: String, pub name: String,
pub namespace: String, pub namespace: String,
} }

View File

@@ -1,5 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use log::error; use k8s_openapi::Resource;
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use serde::Serialize; use serde::Serialize;
use std::str::FromStr; use std::str::FromStr;
@@ -50,7 +50,6 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret {
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
error!("Uncomment below, only disabled for debugging");
self.score self.score
.create_interpret() .create_interpret()
.execute(inventory, topology) .execute(inventory, topology)
@@ -646,7 +645,7 @@ server:
# Argo CD server ingress configuration # Argo CD server ingress configuration
ingress: ingress:
# -- Enable an ingress resource for the Argo CD server # -- Enable an ingress resource for the Argo CD server
enabled: true enabled: false
# -- Specific implementation for ingress controller. One of `generic`, `aws` or `gke` # -- Specific implementation for ingress controller. One of `generic`, `aws` or `gke`
## Additional configuration might be required in related configuration sections ## Additional configuration might be required in related configuration sections
controller: generic controller: generic

View File

@@ -1,100 +1,44 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose}; use log::info;
use log::{debug, info};
use crate::{ use crate::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
application::{ApplicationFeature, OCICompliant}, application::{Application, ApplicationFeature},
monitoring::{ monitoring::{
alert_channel::webhook_receiver::WebhookReceiver, application_monitoring::k8s_application_monitoring_score::ApplicationPrometheusMonitoringScore,
kube_prometheus::{ kube_prometheus::types::{NamespaceSelector, ServiceMonitor}, prometheus::prometheus::Prometheus,
helm_prometheus_alert_score::HelmPrometheusAlertingScore,
types::{NamespaceSelector, ServiceMonitor},
},
ntfy::ntfy::NtfyScore,
}, },
}, },
score::Score, score::Score,
topology::{HelmCommand, K8sclient, Topology, Url, tenant::TenantManager}, topology::{oberservability::monitoring::{AlertReceiver, AlertRule, AlertSender}, tenant::TenantManager, HelmCommand, K8sclient, Topology},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Monitoring { pub struct PrometheusMonitoring {
pub application: Arc<dyn OCICompliant>, pub application: Arc<dyn Application>,
pub alert_receivers: Vec<Box<dyn AlertReceiver<Prometheus>>>,
pub alert_rules: Vec<Box<dyn AlertRule<Prometheus>>>,
} }
#[async_trait] #[async_trait]
impl<T: Topology + HelmCommand + K8sclient + 'static + TenantManager> ApplicationFeature<T> impl<T: Topology + HelmCommand + 'static + TenantManager> ApplicationFeature<T> for PrometheusMonitoring {
for Monitoring
{
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
info!("Ensuring monitoring is available for application"); info!("Ensuring monitoring is available for application");
let ns = self.application.name();
let ntfy = NtfyScore {
// namespace: topology
// .get_tenant_config()
// .await
// .expect("couldn't get tenant config")
// .name,
namespace: self.application.name(),
host: "localhost".to_string(),
};
ntfy.create_interpret()
.execute(&Inventory::empty(), topology)
.await
.expect("couldn't create interpret for ntfy");
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}",
self.application.name()
)
.as_str(),
)
.unwrap(),
),
};
let mut service_monitor = ServiceMonitor::default(); let mut service_monitor = ServiceMonitor::default();
service_monitor.name = ns.clone();
service_monitor.namespace = ns.clone();
service_monitor.namespace_selector = Some(NamespaceSelector { service_monitor.namespace_selector = Some(NamespaceSelector {
any: true, any: true,
match_names: vec![], match_names: vec![ns.clone()],
}); });
let alerting_score = ApplicationPrometheusMonitoringScore {
service_monitor.name = "rust-webapp".to_string(); namespace: ns,
receivers: self.alert_receivers.clone(),
// let alerting_score = ApplicationPrometheusMonitoringScore { rules: self.alert_rules.clone(),
// receivers: vec![Box::new(ntfy_receiver)],
// rules: vec![],
// service_monitors: vec![service_monitor],
// };
let alerting_score = HelmPrometheusAlertingScore {
receivers: vec![Box::new(ntfy_receiver)],
rules: vec![],
service_monitors: vec![service_monitor], service_monitors: vec![service_monitor],
}; };

View File

@@ -59,7 +59,9 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
} }
}; };
} }
Ok(Outcome::success("successfully created app".to_string())) 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 {

View File

@@ -4,17 +4,13 @@ use std::process;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bollard::query_parameters::PushImageOptionsBuilder;
use bollard::{Docker, body_full};
use dockerfile_builder::Dockerfile; use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder; use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt;
use log::{debug, error, info}; use log::{debug, error, info};
use serde::Serialize; use serde::Serialize;
use tar::Archive;
use crate::config::Config; use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::{ use crate::{
score::Score, score::Score,
topology::{Topology, Url}, topology::{Topology, Url},
@@ -112,7 +108,6 @@ impl OCICompliant for RustWebapp {
// 1. Build the local image by calling the synchronous helper function. // 1. Build the local image by calling the synchronous helper function.
let local_image_name = self.local_image_name(); let local_image_name = self.local_image_name();
self.build_docker_image(&local_image_name) self.build_docker_image(&local_image_name)
.await
.map_err(|e| format!("Failed to build Docker image: {}", e))?; .map_err(|e| format!("Failed to build Docker image: {}", e))?;
info!( info!(
"Successfully built local Docker image: {}", "Successfully built local Docker image: {}",
@@ -122,7 +117,6 @@ impl OCICompliant for RustWebapp {
let remote_image_name = self.image_name(); let remote_image_name = self.image_name();
// 2. Push the image to the registry. // 2. Push the image to the registry.
self.push_docker_image(&local_image_name, &remote_image_name) self.push_docker_image(&local_image_name, &remote_image_name)
.await
.map_err(|e| format!("Failed to push Docker image: {}", e))?; .map_err(|e| format!("Failed to push Docker image: {}", e))?;
info!("Successfully pushed Docker image to: {}", remote_image_name); info!("Successfully pushed Docker image to: {}", remote_image_name);
@@ -134,12 +128,10 @@ impl OCICompliant for RustWebapp {
} }
fn image_name(&self) -> String { fn image_name(&self) -> String {
let config = Config::load().expect("couldn't load config");
format!( format!(
"{}/{}/{}", "{}/{}/{}",
config.registry_url, *REGISTRY_URL,
config.registry_project, *REGISTRY_PROJECT,
&self.local_image_name() &self.local_image_name()
) )
} }
@@ -161,68 +153,66 @@ impl RustWebapp {
} }
/// Builds the Docker image using the generated Dockerfile. /// Builds the Docker image using the generated Dockerfile.
pub async fn build_docker_image( pub fn build_docker_image(
&self, &self,
image_name: &str, image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
info!("Generating Dockerfile for '{}'", self.name); info!("Generating Dockerfile for '{}'", self.name);
let _dockerfile_path = self.build_dockerfile()?; let dockerfile_path = self.build_dockerfile()?;
let docker = Docker::connect_with_socket_defaults().unwrap(); info!(
"Building Docker image with file {} from root {}",
let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default() dockerfile_path.to_string_lossy(),
.dockerfile("Dockerfile.harmony") self.project_root.to_string_lossy()
.t(image_name)
.q(false)
.version(bollard::query_parameters::BuilderVersion::BuilderV1)
.platform("linux/x86_64");
let mut temp_tar_builder = tar::Builder::new(Vec::new());
let _ = 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()
.map(|entry| entry.unwrap().path().unwrap().into_owned())
.collect::<Vec<_>>();
debug!("files in docker tar: {:#?}", archived_files);
let mut image_build_stream = docker.build_image(
build_image_options.build(),
None,
Some(body_full(archive.into())),
); );
let output = process::Command::new("docker")
.args([
"build",
"--file",
dockerfile_path.to_str().unwrap(),
"-t",
&image_name,
self.project_root.to_str().unwrap(),
])
.spawn()?
.wait_with_output()?;
while let Some(msg) = image_build_stream.next().await { self.check_output(&output, "Failed to build Docker image")?;
println!("Message: {msg:?}");
}
Ok(image_name.to_string()) Ok(image_name.to_string())
} }
/// Tags and pushes a Docker image to the configured remote registry. /// Tags and pushes a Docker image to the configured remote registry.
async fn push_docker_image( fn push_docker_image(
&self, &self,
image_name: &str, image_name: &str,
full_tag: &str, full_tag: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
info!("Pushing docker image {full_tag}"); info!("Pushing docker image {full_tag}");
let docker = Docker::connect_with_socket_defaults().unwrap(); // Tag the image for the remote registry.
let output = process::Command::new("docker")
.args(["tag", image_name, &full_tag])
.spawn()?
.wait_with_output()?;
self.check_output(&output, "Tagging docker image failed")?;
debug!(
"docker tag output: stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
// let push_options = PushImageOptionsBuilder::new().tag(tag); // Push the image.
let output = process::Command::new("docker")
let mut push_image_stream = .args(["push", &full_tag])
docker.push_image(full_tag, Some(PushImageOptionsBuilder::new().build()), None); .spawn()?
.wait_with_output()?;
while let Some(msg) = push_image_stream.next().await { self.check_output(&output, "Pushing docker image failed")?;
println!("Message: {msg:?}"); debug!(
} "docker push output: stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok(full_tag.to_string()) Ok(full_tag.to_string())
} }
@@ -359,11 +349,7 @@ impl RustWebapp {
image_url: &str, image_url: &str,
) -> Result<PathBuf, Box<dyn std::error::Error>> { ) -> Result<PathBuf, Box<dyn std::error::Error>> {
let chart_name = format!("{}-chart", self.name); let chart_name = format!("{}-chart", self.name);
let chart_dir = self let chart_dir = self.project_root.join("helm").join(&chart_name);
.project_root
.join(".harmony_generated")
.join("helm")
.join(&chart_name);
let templates_dir = chart_dir.join("templates"); let templates_dir = chart_dir.join("templates");
fs::create_dir_all(&templates_dir)?; fs::create_dir_all(&templates_dir)?;
@@ -430,7 +416,7 @@ ingress:
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 }}
{{/* {{/*
@@ -438,7 +424,7 @@ 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 }}
"#; "#;
@@ -451,12 +437,12 @@ 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 - port: {{ .Values.service.port }}
port: {{ $.Values.service.port | default 3000 }} targetPort: 3000
targetPort: {{ $.Values.service.port | default 3000 }}
protocol: TCP protocol: TCP
name: http
selector: selector:
app: {{ include "chart.name" . }} app: {{ include "chart.name" . }}
"#; "#;
@@ -469,7 +455,7 @@ 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" . }}
@@ -480,28 +466,28 @@ spec:
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: http
containerPort: {{ $.Values.service.port | default 3000 }} containerPort: 3000
protocol: TCP protocol: TCP
"#; "#;
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 = r#" let ingress_yaml = 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 }}
@@ -510,7 +496,7 @@ spec:
{{- end }} {{- end }}
{{- end }} {{- end }}
rules: rules:
{{- range $.Values.ingress.hosts }} {{- range .Values.ingress.hosts }}
- host: {{ .host | quote }} - host: {{ .host | quote }}
http: http:
paths: paths:
@@ -521,7 +507,7 @@ spec:
service: service:
name: {{ include "chart.fullname" $ }} name: {{ include "chart.fullname" $ }}
port: port:
number: {{ $.Values.service.port | default 3000 }} number: 3000
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
@@ -540,15 +526,11 @@ spec:
info!( info!(
"Launching `helm package {}` cli with CWD {}", "Launching `helm package {}` cli with CWD {}",
chart_dirname.to_string_lossy(), chart_dirname.to_string_lossy(),
&self &self.project_root.join("helm").to_string_lossy()
.project_root
.join(".harmony_generated")
.join("helm")
.to_string_lossy()
); );
let output = process::Command::new("helm") let output = process::Command::new("helm")
.args(["package", chart_dirname.to_str().unwrap()]) .args(["package", chart_dirname.to_str().unwrap()])
.current_dir(&self.project_root.join(".harmony_generated").join("helm")) // Run package from the parent dir .current_dir(&self.project_root.join("helm")) // Run package from the parent dir
.output()?; .output()?;
self.check_output(&output, "Failed to package Helm chart")?; self.check_output(&output, "Failed to package Helm chart")?;
@@ -565,11 +547,7 @@ spec:
} }
// The output from helm is relative, so we join it with the execution directory. // The output from helm is relative, so we join it with the execution directory.
Ok(self Ok(self.project_root.join("helm").join(tgz_name))
.project_root
.join(".harmony_generated")
.join("helm")
.join(tgz_name))
} }
/// Pushes a packaged Helm chart to an OCI registry. /// Pushes a packaged Helm chart to an OCI registry.
@@ -577,11 +555,9 @@ spec:
&self, &self,
packaged_chart_path: &PathBuf, packaged_chart_path: &PathBuf,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let config = Config::load().expect("couldn't load config");
// The chart name is the file stem of the .tgz file // The chart name is the file stem of the .tgz file
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://{}/{}", config.registry_url, config.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);
info!( info!(

View File

@@ -220,7 +220,6 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
yaml_path, yaml_path,
Some(&helm_options), Some(&helm_options),
); );
let status = match res { let status = match res {
Ok(status) => status, Ok(status) => status,
Err(err) => return Err(InterpretError::new(err.to_string())), Err(err) => return Err(InterpretError::new(err.to_string())),

View File

@@ -10,25 +10,14 @@ use crate::{
topology::{HttpServer, Topology, Url}, topology::{HttpServer, Topology, Url},
}; };
/// Configure an HTTP server that is provided by the Topology
///
/// This Score will let you easily specify a file path to be served by the HTTP server
///
/// For example, if you have a folder of assets at `/var/www/assets` simply do :
///
/// ```rust,ignore
/// StaticFilesHttpScore {
/// files_to_serve: url!("file:///var/www/assets"),
/// }
/// ```
#[derive(Debug, new, Clone, Serialize)] #[derive(Debug, new, Clone, Serialize)]
pub struct StaticFilesHttpScore { pub struct HttpScore {
files_to_serve: Url, files_to_serve: Url,
} }
impl<T: Topology + HttpServer> Score<T> for StaticFilesHttpScore { impl<T: Topology + HttpServer> Score<T> for HttpScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(StaticFilesHttpInterpret::new(self.clone())) Box::new(HttpInterpret::new(self.clone()))
} }
fn name(&self) -> String { fn name(&self) -> String {
@@ -37,12 +26,12 @@ impl<T: Topology + HttpServer> Score<T> for StaticFilesHttpScore {
} }
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone)]
pub struct StaticFilesHttpInterpret { pub struct HttpInterpret {
score: StaticFilesHttpScore, score: HttpScore,
} }
#[async_trait] #[async_trait]
impl<T: Topology + HttpServer> Interpret<T> for StaticFilesHttpInterpret { impl<T: Topology + HttpServer> Interpret<T> for HttpInterpret {
async fn execute( async fn execute(
&self, &self,
_inventory: &Inventory, _inventory: &Inventory,

View File

@@ -5,7 +5,7 @@ use log::info;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
config::Config, config::HARMONY_DATA_DIR,
data::{Id, Version}, data::{Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
@@ -21,10 +21,8 @@ pub struct K3DInstallationScore {
impl Default for K3DInstallationScore { impl Default for K3DInstallationScore {
fn default() -> Self { fn default() -> Self {
let config = Config::load().expect("couldn't load config");
Self { Self {
installation_path: config.data_dir.join("k3d"), installation_path: HARMONY_DATA_DIR.join("k3d"),
cluster_name: "harmony".to_string(), cluster_name: "harmony".to_string(),
} }
} }

View File

@@ -14,7 +14,7 @@ use async_trait::async_trait;
use log::{debug, info}; use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use crate::config::Config as HarmonyConfig; use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::modules::k8s::ingress::K8sIngressScore; use crate::modules::k8s::ingress::K8sIngressScore;
use crate::topology::HelmCommand; use crate::topology::HelmCommand;
use crate::{ use crate::{
@@ -355,12 +355,7 @@ opcache.fast_shutdown=1
} }
fn push_docker_image(&self, image_name: &str) -> Result<String, Box<dyn std::error::Error>> { fn push_docker_image(&self, image_name: &str) -> Result<String, Box<dyn std::error::Error>> {
let config = HarmonyConfig::load().expect("couldn't load config"); let full_tag = format!("{}/{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT, &image_name);
let full_tag = format!(
"{}/{}/{}",
config.registry_url, config.registry_project, &image_name
);
let output = std::process::Command::new("docker") let output = std::process::Command::new("docker")
.args(["tag", image_name, &full_tag]) .args(["tag", image_name, &full_tag])
.output()?; .output()?;

View File

@@ -1,3 +1,5 @@
use std::any::Any;
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize; use serde::Serialize;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
@@ -11,7 +13,10 @@ use crate::{
}, },
prometheus::prometheus::{Prometheus, PrometheusReceiver}, prometheus::prometheus::{Prometheus, PrometheusReceiver},
}, },
topology::{Url, oberservability::monitoring::AlertReceiver}, topology::{
Url,
oberservability::monitoring::{AlertReceiver, AlertSender},
},
}; };
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]

View File

@@ -1,22 +1,22 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use log::debug;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
modules::monitoring::{ modules::monitoring::{
kube_prometheus::types::ServiceMonitor, kube_prometheus::types::ServiceMonitor,
prometheus::{prometheus::Prometheus, prometheus_config::PrometheusConfig}, prometheus::{prometheus::Prometheus, prometheus_config::HelmPrometheusConfig},
}, },
score::Score, score::Score,
topology::{ topology::{
HelmCommand, Topology, oberservability::monitoring::{AlertReceiver, AlertRule, AlertingInterpret}, tenant::TenantManager, HelmCommand, K8sclient, Topology
oberservability::monitoring::{AlertReceiver, AlertRule, AlertingInterpret},
tenant::TenantManager,
}, },
}; };
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct ApplicationPrometheusMonitoringScore { pub struct ApplicationPrometheusMonitoringScore {
pub namespace: String,
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>, pub service_monitors: Vec<ServiceMonitor>,
@@ -24,16 +24,17 @@ pub struct ApplicationPrometheusMonitoringScore {
impl<T: Topology + HelmCommand + TenantManager> Score<T> for ApplicationPrometheusMonitoringScore { impl<T: Topology + HelmCommand + TenantManager> Score<T> for ApplicationPrometheusMonitoringScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
let mut prom_config = PrometheusConfig::new(); let config = Arc::new(Mutex::new(HelmPrometheusConfig::new()));
prom_config.alert_manager = true;
let config = Arc::new(Mutex::new(prom_config));
config config
.try_lock() .try_lock()
.expect("couldn't lock config") .expect("couldn't lock config")
.additional_service_monitors = self.service_monitors.clone(); .additional_service_monitors = self.service_monitors.clone();
let ns = self.namespace.clone();
config.try_lock().expect("couldn't lock config").namespace = Some(ns.clone());
debug!("set namespace to {}", ns);
Box::new(AlertingInterpret { Box::new(AlertingInterpret {
sender: Prometheus::new(), sender: Prometheus { config },
receivers: self.receivers.clone(), receivers: self.receivers.clone(),
rules: self.rules.clone(), rules: self.rules.clone(),
}) })

View File

@@ -1,6 +1,5 @@
use non_blank_string_rs::NonBlankString;
use std::str::FromStr; use std::str::FromStr;
use non_blank_string_rs::NonBlankString;
use crate::modules::helm::chart::HelmChartScore; use crate::modules::helm::chart::HelmChartScore;
pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore { pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore {
@@ -8,10 +7,46 @@ pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore {
r#" r#"
rbac: rbac:
namespaced: true namespaced: true
sidecar:
dashboards: datasources:
enabled: true datasources.yaml:
"# apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus-server.{ns}.svc.cluster.local
isDefault: true
dashboardProviders:
dashboardproviders.yaml:
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
editable: true
options:
path: /var/lib/grafana/dashboards/default
dashboards:
default:
compute-usage:
url: https://grafana.com/api/dashboards/315/revisions/1/download
pod-health:
url: https://grafana.com/api/dashboards/15758/revisions/1/download
namespace-resources:
url: https://grafana.com/api/dashboards/9809/revisions/1/download
namespace-resources-vs-quotas:
url: https://grafana.com/api/dashboards/17044/revisions/1/download
persistent-volume-usage:
url: https://grafana.com/api/dashboards/7685/revisions/1/download
"#,
ns = ns
); );
HelmChartScore { HelmChartScore {
@@ -20,9 +55,10 @@ sidecar:
chart_name: NonBlankString::from_str("oci://ghcr.io/grafana/helm-charts/grafana").unwrap(), chart_name: NonBlankString::from_str("oci://ghcr.io/grafana/helm-charts/grafana").unwrap(),
chart_version: None, chart_version: None,
values_overrides: None, values_overrides: None,
values_yaml: Some(values.to_string()), values_yaml: Some(values),
create_namespace: true, create_namespace: true,
install_only: true, install_only: false,
repository: None, repository: None,
} }
} }

View File

@@ -38,15 +38,15 @@ impl KubePrometheusConfig {
node_exporter: false, node_exporter: false,
prometheus: true, prometheus: true,
kubernetes_service_monitors: true, kubernetes_service_monitors: true,
kubernetes_api_server: true, kubernetes_api_server: false,
kubelet: true, kubelet: true,
kube_controller_manager: true, kube_controller_manager: false,
kube_etcd: true, kube_etcd: false,
kube_proxy: true, kube_proxy: false,
kube_state_metrics: true, kube_state_metrics: true,
prometheus_operator: true, prometheus_operator: true,
core_dns: true, core_dns: false,
kube_scheduler: true, kube_scheduler: false,
alert_receiver_configs: vec![], alert_receiver_configs: vec![],
alert_rules: vec![], alert_rules: vec![],
additional_service_monitors: vec![], additional_service_monitors: vec![],

View File

@@ -70,12 +70,12 @@ pub fn kube_prometheus_helm_chart_score(
r#" r#"
global: global:
rbac: rbac:
create: true create: false
prometheus: prometheus:
enabled: {prometheus} enabled: {prometheus}
prometheusSpec: prometheusSpec:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 500Mi memory: 500Mi
limits: limits:
@@ -121,7 +121,7 @@ defaultRules:
windowsMonitoring: windowsMonitoring:
enabled: {windows_monitoring} enabled: {windows_monitoring}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -130,13 +130,13 @@ windowsMonitoring:
grafana: grafana:
enabled: {grafana} enabled: {grafana}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
cpu: 200m cpu: 200m
memory: 250Mi memory: 250Mi
initChownData: initChownData:
resources: resources:
requests: requests:
cpu: 10m cpu: 10m
@@ -157,7 +157,7 @@ kubernetesServiceMonitors:
kubeApiServer: kubeApiServer:
enabled: {kubernetes_api_server} enabled: {kubernetes_api_server}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -166,7 +166,7 @@ kubeApiServer:
kubelet: kubelet:
enabled: {kubelet} enabled: {kubelet}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -175,7 +175,7 @@ kubelet:
kubeControllerManager: kubeControllerManager:
enabled: {kube_controller_manager} enabled: {kube_controller_manager}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -184,7 +184,7 @@ kubeControllerManager:
coreDns: coreDns:
enabled: {core_dns} enabled: {core_dns}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -193,7 +193,7 @@ coreDns:
kubeEtcd: kubeEtcd:
enabled: {kube_etcd} enabled: {kube_etcd}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -202,7 +202,7 @@ kubeEtcd:
kubeScheduler: kubeScheduler:
enabled: {kube_scheduler} enabled: {kube_scheduler}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -211,7 +211,7 @@ kubeScheduler:
kubeProxy: kubeProxy:
enabled: {kube_proxy} enabled: {kube_proxy}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -221,7 +221,7 @@ kubeStateMetrics:
enabled: {kube_state_metrics} enabled: {kube_state_metrics}
kube-state-metrics: kube-state-metrics:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -230,7 +230,7 @@ kube-state-metrics:
nodeExporter: nodeExporter:
enabled: {node_exporter} enabled: {node_exporter}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -238,16 +238,16 @@ nodeExporter:
memory: 250Mi memory: 250Mi
prometheus-node-exporter: prometheus-node-exporter:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
cpu: 200m cpu: 200m
memory: 250Mi memory: 250Mi
prometheusOperator: prometheusOperator:
enabled: true enabled: false
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -255,7 +255,7 @@ prometheusOperator:
memory: 200Mi memory: 200Mi
prometheusConfigReloader: prometheusConfigReloader:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -267,7 +267,7 @@ prometheusOperator:
limits: limits:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
requests: requests:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
patch: patch:
@@ -275,7 +275,7 @@ prometheusOperator:
limits: limits:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
requests: requests:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
"#, "#,

View File

@@ -28,7 +28,7 @@ impl<T: Topology + HelmCommand + TenantManager> Score<T> for HelmPrometheusAlert
.expect("couldn't lock config") .expect("couldn't lock config")
.additional_service_monitors = self.service_monitors.clone(); .additional_service_monitors = self.service_monitors.clone();
Box::new(AlertingInterpret { Box::new(AlertingInterpret {
sender: KubePrometheus { config }, sender: KubePrometheus::new(),
receivers: self.receivers.clone(), receivers: self.receivers.clone(),
rules: self.rules.clone(), rules: self.rules.clone(),
}) })

View File

@@ -211,6 +211,8 @@ pub struct Selector {
pub struct ServiceMonitor { pub struct ServiceMonitor {
pub name: String, pub name: String,
pub namespace: String,
// # Additional labels to set used for the ServiceMonitorSelector. Together with standard labels from the chart // # Additional labels to set used for the ServiceMonitorSelector. Together with standard labels from the chart
pub additional_labels: Option<HashMap<String, String>>, pub additional_labels: Option<HashMap<String, String>>,
@@ -261,6 +263,7 @@ impl Default for ServiceMonitor {
fn default() -> Self { fn default() -> Self {
Self { Self {
name: Default::default(), name: Default::default(),
namespace: Default::default(),
additional_labels: Default::default(), additional_labels: Default::default(),
job_label: Default::default(), job_label: Default::default(),
target_labels: Default::default(), target_labels: Default::default(),

View File

@@ -3,7 +3,7 @@ use std::str::FromStr;
use crate::modules::helm::chart::{HelmChartScore, HelmRepository}; use crate::modules::helm::chart::{HelmChartScore, HelmRepository};
pub fn ntfy_helm_chart_score(namespace: String, host: String) -> HelmChartScore { pub fn ntfy_helm_chart_score(namespace: String) -> HelmChartScore {
let values = format!( let values = format!(
r#" r#"
replicaCount: 1 replicaCount: 1
@@ -28,12 +28,12 @@ service:
port: 80 port: 80
ingress: ingress:
enabled: true enabled: false
# annotations: # annotations:
# kubernetes.io/ingress.class: nginx # kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true" # kubernetes.io/tls-acme: "true"
hosts: hosts:
- host: {host} - host: ntfy.host.com
paths: paths:
- path: / - path: /
pathType: ImplementationSpecific pathType: ImplementationSpecific

View File

@@ -17,7 +17,6 @@ use crate::{
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct NtfyScore { pub struct NtfyScore {
pub namespace: String, pub namespace: String,
pub host: String,
} }
impl<T: Topology + HelmCommand + K8sclient> Score<T> for NtfyScore { impl<T: Topology + HelmCommand + K8sclient> Score<T> for NtfyScore {
@@ -127,7 +126,7 @@ impl<T: Topology + HelmCommand + K8sclient> Interpret<T> for NtfyInterpret {
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
ntfy_helm_chart_score(self.score.namespace.clone(), self.score.host.clone()) ntfy_helm_chart_score(self.score.namespace.clone())
.create_interpret() .create_interpret()
.execute(inventory, topology) .execute(inventory, topology)
.await?; .await?;

View File

@@ -1 +1,2 @@
pub mod prometheus_helm; pub mod prometheus_helm;
pub mod types;

View File

@@ -1,37 +1,145 @@
use std::collections::BTreeMap;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use log::debug;
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use serde_yaml::{Mapping, Value};
use crate::modules::{ use crate::modules::helm::chart::HelmChartScore;
helm::chart::HelmChartScore, monitoring::prometheus::prometheus_config::PrometheusConfig, use crate::modules::monitoring::kube_prometheus::types::{
AlertGroup, AlertManager, AlertManagerConfig, AlertManagerRoute, AlertManagerSpec,
ConfigReloader, Limits, Requests, Resources,
}; };
use crate::modules::monitoring::prometheus::helm::types::{
AlertFile, EnabledConfig, KsmRbacConfig, KubeStateMetricsConfig, LabelSelector, Monitor,
Prometheus, PrometheusHelmValues, RbacConfig, ServerConfig, ServerRbacConfig,
};
use crate::modules::monitoring::prometheus::prometheus_config::HelmPrometheusConfig;
pub fn prometheus_helm_chart_score(config: Arc<Mutex<PrometheusConfig>>) -> HelmChartScore { pub fn prometheus_helm_chart_score(config: Arc<Mutex<HelmPrometheusConfig>>) -> HelmChartScore {
let config = config.lock().unwrap(); let config = config.lock().unwrap();
let ns = config.namespace.clone().unwrap(); let ns = config.namespace.clone().unwrap();
let values = format!(
r#" let rbac_config = RbacConfig { create: false };
rbac:
create: true let ksm_config = KubeStateMetricsConfig {
kube-state-metrics: enabled: true,
enabled: false rbac: KsmRbacConfig {
nodeExporter: use_cluster_role: false,
enabled: false },
alertmanager: prometheus: Prometheus {
enabled: false monitor: Monitor { enabled: true },
pushgateway: },
enabled: false };
server:
serviceAccount: let mut selector_labels = BTreeMap::new();
create: false selector_labels.insert("kubernetes.io/metadata.name".to_string(), ns.clone());
rbac: let mut kube_state_metrics_labels = BTreeMap::new();
create: true kube_state_metrics_labels.insert(
fullnameOverride: prometheus-{ns} "app.kubernetes.io/name".to_string(),
"# "kube-state-metrics".to_string(),
); );
let selector = LabelSelector {
match_labels: selector_labels,
};
let server_config = ServerConfig {
namespaces: vec![ns.clone()],
use_existing_cluster_role_name: false,
};
let mut null_receiver = Mapping::new();
null_receiver.insert(
Value::String("receiver".to_string()),
Value::String("default-receiver".to_string()),
);
null_receiver.insert(
Value::String("matchers".to_string()),
Value::Sequence(vec![Value::String("alertname!=Watchdog".to_string())]),
);
null_receiver.insert(Value::String("continue".to_string()), Value::Bool(true));
let mut alert_manager_channel_config = AlertManagerConfig {
global: Mapping::new(),
route: AlertManagerRoute {
routes: vec![Value::Mapping(null_receiver)],
},
receivers: vec![serde_yaml::from_str("name: 'default-receiver'").unwrap()],
};
for receiver in config.alert_receiver_configs.iter() {
if let Some(global) = receiver.channel_global_config.clone() {
alert_manager_channel_config
.global
.insert(global.0, global.1);
}
alert_manager_channel_config
.route
.routes
.push(receiver.channel_route.clone());
alert_manager_channel_config
.receivers
.push(receiver.channel_receiver.clone());
}
let alert_manager_values = AlertManager {
enabled: config.alert_manager,
config: alert_manager_channel_config,
alertmanager_spec: AlertManagerSpec {
resources: Resources {
limits: Limits {
memory: "100Mi".to_string(),
cpu: "100m".to_string(),
},
requests: Requests {
memory: "100Mi".to_string(),
cpu: "100m".to_string(),
},
},
},
init_config_reloader: ConfigReloader {
resources: Resources {
limits: Limits {
memory: "100Mi".to_string(),
cpu: "100m".to_string(),
},
requests: Requests {
memory: "100Mi".to_string(),
cpu: "100m".to_string(),
},
},
},
};
let mut result: BTreeMap<String, AlertFile> = BTreeMap::new();
for rule in config.alert_rules.clone().iter() {
for (name, group) in &rule.rules {
result
.entry("alerting_rules.yml".to_string())
.and_modify(|e| e.groups.extend(group.groups.clone()))
.or_insert(AlertFile {
groups: group.groups.clone(),
});
}
}
let final_values = PrometheusHelmValues {
rbac: rbac_config,
kube_state_metrics: ksm_config,
server: server_config,
alertmanager: alert_manager_values,
server_files: result,
additional_service_monitors: config.additional_service_monitors.clone(),
prometheus_node_exporter: EnabledConfig { enabled: false },
prometheus_pushgateway: EnabledConfig { enabled: false },
};
let values_yaml =
serde_yaml::to_string(&final_values).expect("Failed to serialize final Helm values");
debug!("full values.yaml: \n{}", values_yaml);
HelmChartScore { HelmChartScore {
namespace: Some(NonBlankString::from_str(&config.namespace.clone().unwrap()).unwrap()), namespace: Some(NonBlankString::from_str(&ns).unwrap()),
release_name: NonBlankString::from_str("prometheus").unwrap(), release_name: NonBlankString::from_str("prometheus").unwrap(),
chart_name: NonBlankString::from_str( chart_name: NonBlankString::from_str(
"oci://ghcr.io/prometheus-community/charts/prometheus", "oci://ghcr.io/prometheus-community/charts/prometheus",
@@ -39,7 +147,7 @@ fullnameOverride: prometheus-{ns}
.unwrap(), .unwrap(),
chart_version: None, chart_version: None,
values_overrides: None, values_overrides: None,
values_yaml: Some(values.to_string()), values_yaml: Some(values_yaml),
create_namespace: true, create_namespace: true,
install_only: true, install_only: true,
repository: None, repository: None,

View File

@@ -0,0 +1,94 @@
use std::collections::BTreeMap;
use serde::Serialize;
use crate::modules::monitoring::{alert_rule::prometheus_alert_rule::AlertManagerRuleGroup, kube_prometheus::types::{
AlertGroup, AlertManager, AlertManagerAdditionalPromRules, AlertManagerValues, ServiceMonitor
}};
#[derive(Debug, Clone, Serialize)]
pub struct RuleFilesConfig {
#[serde(rename = "ruleFiles")]
pub files: BTreeMap<String, AlertGroup>,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrometheusHelmValues {
pub rbac: RbacConfig,
#[serde(rename = "kube-state-metrics")]
pub kube_state_metrics: KubeStateMetricsConfig,
pub server: ServerConfig,
pub alertmanager: AlertManager, // You already have this
#[serde(rename = "serverFiles")]
pub server_files: BTreeMap<String, AlertFile>, // You already have this
pub additional_service_monitors: Vec<ServiceMonitor>, // You already have this
#[serde(rename = "prometheus-node-exporter")]
pub prometheus_node_exporter: EnabledConfig,
#[serde(rename = "prometheus-pushgateway")]
pub prometheus_pushgateway: EnabledConfig,
}
#[derive(Serialize, Debug, Clone)]
pub struct AlertFile {
pub groups: Vec<AlertManagerRuleGroup>,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RbacConfig {
pub create: bool,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct KubeStateMetricsConfig {
pub enabled: bool,
pub rbac: KsmRbacConfig,
pub prometheus: Prometheus,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Prometheus {
pub monitor: Monitor
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Monitor{
pub enabled: bool
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct KsmRbacConfig {
pub use_cluster_role: bool,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ServerConfig {
pub namespaces: Vec<String>,
pub use_existing_cluster_role_name: bool,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ServerRbacConfig {
pub create: bool,
pub use_cluster_role: bool,
pub namespaced: bool,
}
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LabelSelector {
#[serde(rename = "matchLabels")]
pub match_labels: BTreeMap<String, String>,
}
#[derive(Serialize, Debug)]
pub struct EnabledConfig {
pub enabled: bool,
}

View File

@@ -14,7 +14,7 @@ use crate::{
}, },
score::Score, score::Score,
topology::{ topology::{
HelmCommand, Topology, HelmCommand, K8sclient, Topology,
installable::Installable, installable::Installable,
oberservability::monitoring::{AlertReceiver, AlertRule, AlertSender}, oberservability::monitoring::{AlertReceiver, AlertRule, AlertSender},
tenant::TenantManager, tenant::TenantManager,
@@ -22,12 +22,12 @@ use crate::{
}; };
use super::{ use super::{
helm::prometheus_helm::prometheus_helm_chart_score, prometheus_config::PrometheusConfig, helm::prometheus_helm::prometheus_helm_chart_score, prometheus_config::HelmPrometheusConfig,
}; };
#[derive(Debug)] #[derive(Debug)]
pub struct Prometheus { pub struct Prometheus {
pub config: Arc<Mutex<PrometheusConfig>>, pub config: Arc<Mutex<HelmPrometheusConfig>>,
} }
#[async_trait] #[async_trait]
@@ -40,18 +40,17 @@ impl AlertSender for Prometheus {
impl Prometheus { impl Prometheus {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
config: Arc::new(Mutex::new(PrometheusConfig::new())), config: Arc::new(Mutex::new(HelmPrometheusConfig::new())),
} }
} }
pub async fn configure_with_topology<T: TenantManager>(&self, topology: &T) { pub async fn configure_with_topology<T: TenantManager>(&self, topology: &T) {
let ns = topology if let Some(cfg) = topology.get_tenant_config().await {
.get_tenant_config() debug!("Overriding namespace with tenant config: {}", cfg.name);
.await self.config.lock().unwrap().namespace = Some(cfg.name.clone());
.map(|cfg| cfg.name.clone()) } else {
.unwrap_or_else(|| "monitoring".to_string()); debug!("No tenant config found; keeping existing namespace.");
}
error!("This must be refactored, see comments in pr #74"); error!("This must be refactored, see comments in pr #74");
debug!("NS: {}", ns);
self.config.lock().unwrap().namespace = Some(ns);
} }
pub async fn install_receiver( pub async fn install_receiver(

View File

@@ -3,9 +3,8 @@ use crate::modules::monitoring::kube_prometheus::types::{
}; };
#[derive(Debug)] #[derive(Debug)]
pub struct PrometheusConfig { pub struct HelmPrometheusConfig {
pub namespace: Option<String>, pub namespace: Option<String>,
pub default_rules: bool,
pub alert_manager: bool, pub alert_manager: bool,
pub node_exporter: bool, pub node_exporter: bool,
pub kube_state_metrics: bool, pub kube_state_metrics: bool,
@@ -16,11 +15,10 @@ pub struct PrometheusConfig {
pub additional_service_monitors: Vec<ServiceMonitor>, pub additional_service_monitors: Vec<ServiceMonitor>,
} }
impl PrometheusConfig { impl HelmPrometheusConfig {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
namespace: None, namespace: None,
default_rules: true,
alert_manager: true, alert_manager: true,
node_exporter: false, node_exporter: false,
kube_state_metrics: false, kube_state_metrics: false,

View File

@@ -1 +1,2 @@
pub mod pvc; pub mod pvc;
pub mod pod;

View File

@@ -0,0 +1,38 @@
use crate::modules::monitoring::alert_rule::prometheus_alert_rule::PrometheusAlertRule;
pub fn pod_in_failed_state() -> PrometheusAlertRule {
PrometheusAlertRule::new(
"PodInFailedState",
// This expression checks for any pod where the status phase is 'Failed' and the value is 1 (true).
"kube_pod_status_phase{phase=\"Failed\"} == 1",
)
.for_duration("1m") // Fire if the pod is in this state for 1 minute.
.label("severity", "critical") // A failed pod is a critical issue.
.annotation(
"summary",
"Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} has failed.",
)
.annotation(
"description",
"The pod {{ $labels.pod }} in namespace {{ $labels.namespace }} has entered the 'Failed' state. This is a terminal error and the pod will not be automatically restarted. Please check the pod logs to diagnose the issue.",
)
}
pub fn pod_restarting_frequently() -> PrometheusAlertRule {
PrometheusAlertRule::new(
"PodRestartingFrequently",
// This expression calculates the increase in the restart count over the last 30 minutes.
// Alert if a container has restarted more than 5 times.
"increase(kube_pod_container_status_restarts_total[30m]) > 5",
)
.for_duration("15m") // The condition must persist for 15 minutes to avoid alerts for minor flaps.
.label("severity", "critical") // A crash-looping pod is effectively down.
.annotation(
"summary",
"Container {{ $labels.container }} in pod {{ $labels.pod }} is restarting frequently.",
)
.annotation(
"description",
"The container '{{ $labels.container }}' in pod '{{ $labels.pod }}' (namespace '{{ $labels.namespace }}') has restarted more than 5 times in the last 30 minutes. The pod is likely in a CrashLoopBackOff state.",
)
}

View File

@@ -83,7 +83,6 @@ pub struct Interface {
pub adv_dhcp_config_advanced: Option<MaybeString>, pub adv_dhcp_config_advanced: Option<MaybeString>,
pub adv_dhcp_config_file_override: Option<MaybeString>, pub adv_dhcp_config_file_override: Option<MaybeString>,
pub adv_dhcp_config_file_override_path: Option<MaybeString>, pub adv_dhcp_config_file_override_path: Option<MaybeString>,
pub mtu: Option<u32>,
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,6 +1,6 @@
use crate::HAProxy; use crate::HAProxy;
use crate::{data::dhcpd::DhcpInterface, xml_utils::to_xml_str}; use crate::{data::dhcpd::DhcpInterface, xml_utils::to_xml_str};
use log::{debug, error}; use log::error;
use uuid::Uuid; use uuid::Uuid;
use yaserde::{MaybeString, NamedList, RawXml}; use yaserde::{MaybeString, NamedList, RawXml};
use yaserde_derive::{YaDeserialize, YaSerialize}; use yaserde_derive::{YaDeserialize, YaSerialize};
@@ -17,12 +17,12 @@ pub struct OPNsense {
pub dhcpd: NamedList<DhcpInterface>, pub dhcpd: NamedList<DhcpInterface>,
pub snmpd: Snmpd, pub snmpd: Snmpd,
pub syslog: Syslog, pub syslog: Syslog,
pub nat: Option<Nat>, pub nat: Nat,
pub filter: Filters, pub filter: Filters,
pub load_balancer: Option<LoadBalancer>, pub load_balancer: Option<LoadBalancer>,
pub rrd: Option<RawXml>, pub rrd: Option<RawXml>,
pub ntpd: Ntpd, pub ntpd: Ntpd,
pub widgets: Option<Widgets>, pub widgets: Widgets,
pub revision: Revision, pub revision: Revision,
#[yaserde(rename = "OPNsense")] #[yaserde(rename = "OPNsense")]
pub opnsense: OPNsenseXmlSection, pub opnsense: OPNsenseXmlSection,
@@ -46,12 +46,10 @@ pub struct OPNsense {
pub pischem: Option<Pischem>, pub pischem: Option<Pischem>,
pub ifgroups: Ifgroups, pub ifgroups: Ifgroups,
pub dnsmasq: Option<RawXml>, pub dnsmasq: Option<RawXml>,
pub wizardtemp: Option<RawXml>,
} }
impl From<String> for OPNsense { impl From<String> for OPNsense {
fn from(content: String) -> Self { fn from(content: String) -> Self {
debug!("XML content: {content}");
yaserde::de::from_str(&content) yaserde::de::from_str(&content)
.map_err(|e| println!("{}", e.to_string())) .map_err(|e| println!("{}", e.to_string()))
.expect("OPNSense received invalid string, should be full XML") .expect("OPNSense received invalid string, should be full XML")
@@ -244,7 +242,6 @@ pub struct Ssh {
pub passwordauth: u8, pub passwordauth: u8,
pub keysig: MaybeString, pub keysig: MaybeString,
pub permitrootlogin: u8, pub permitrootlogin: u8,
pub rekeylimit: MaybeString,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
@@ -274,7 +271,6 @@ pub struct Group {
pub member: Vec<u32>, pub member: Vec<u32>,
#[yaserde(rename = "priv")] #[yaserde(rename = "priv")]
pub priv_field: String, pub priv_field: String,
pub source_networks: Vec<MaybeString>,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
@@ -1510,7 +1506,7 @@ pub struct Vlans {
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Bridges { pub struct Bridges {
pub bridged: Option<MaybeString>, pub bridged: MaybeString,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]

View File

@@ -22,4 +22,4 @@ tokio-util = { version = "0.7.13", features = [ "codec" ] }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
[dev-dependencies] [dev-dependencies]
pretty_assertions.workspace = true pretty_assertions = "1.4.1"