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
log/
*.tgz
.gitignore

153
Cargo.lock generated
View File

@@ -219,15 +219,6 @@ dependencies = [
"syn",
]
[[package]]
name = "atomic"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
dependencies = [
"bytemuck",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -418,12 +409,6 @@ version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytemuck"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -1358,7 +1343,6 @@ dependencies = [
"cidr",
"env_logger",
"harmony",
"harmony_cli",
"harmony_macros",
"harmony_tui",
"harmony_types",
@@ -1371,7 +1355,6 @@ dependencies = [
name = "example-rust"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"env_logger",
"harmony",
"harmony_cli",
@@ -1444,31 +1427,6 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "flate2"
version = "1.1.2"
@@ -1768,8 +1726,6 @@ name = "harmony"
version = "0.1.0"
dependencies = [
"async-trait",
"base64 0.22.1",
"bollard",
"chrono",
"cidr",
"convert_case",
@@ -1779,7 +1735,6 @@ dependencies = [
"dyn-clone",
"email_address",
"env_logger",
"figment",
"fqdn",
"futures-util",
"harmony_macros",
@@ -1797,7 +1752,6 @@ dependencies = [
"non-blank-string-rs",
"opnsense-config",
"opnsense-config-xml",
"pretty_assertions",
"rand 0.9.1",
"reqwest 0.11.27",
"russh",
@@ -1806,11 +1760,9 @@ dependencies = [
"serde",
"serde-value",
"serde_json",
"serde_with",
"serde_yaml",
"similar",
"strum 0.27.1",
"tar",
"temp-dir",
"temp-file",
"tempfile",
@@ -2445,12 +2397,6 @@ version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "inout"
version = "0.1.4"
@@ -2780,7 +2726,6 @@ checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
dependencies = [
"bitflags 2.9.1",
"libc",
"redox_syscall",
]
[[package]]
@@ -3272,29 +3217,6 @@ dependencies = [
"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]]
name = "pem"
version = "3.0.5"
@@ -3557,19 +3479,6 @@ dependencies = [
"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]]
name = "punycode"
version = "0.4.1"
@@ -4171,18 +4080,6 @@ dependencies = [
"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]]
name = "scopeguard"
version = "1.2.0"
@@ -4383,36 +4280,22 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.14.0"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.10.0",
"schemars 0.9.0",
"schemars 1.0.3",
"schemars",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"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]]
name = "serde_yaml"
version = "0.9.34+deprecated"
@@ -4785,17 +4668,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "temp-dir"
version = "0.1.16"
@@ -5212,15 +5084,6 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "unicode-ident"
version = "1.0.18"
@@ -5850,16 +5713,6 @@ dependencies = [
"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]]
name = "xml-rs"
version = "0.8.26"

View File

@@ -52,8 +52,3 @@ convert_case = "0.8"
chrono = "0.4"
similar = "2"
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,
maestro::Maestro,
modules::{
http::StaticFilesHttpScore,
http::HttpScore,
ipxe::IpxeScore,
okd::{
bootstrap_dhcp::OKDBootstrapDhcpScore,
@@ -126,7 +126,7 @@ async fn main() {
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
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(),
));
let ipxe_score = IpxeScore::new();

View File

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

View File

@@ -16,4 +16,3 @@ harmony_macros = { path = "../../harmony_macros" }
log = { workspace = true }
env_logger = { workspace = true }
url = { workspace = true }
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }

View File

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

View File

@@ -3,9 +3,18 @@ use std::{path::PathBuf, sync::Arc};
use harmony::{
inventory::Inventory,
maestro::Maestro,
modules::application::{
ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring},
modules::{
application::{
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},
};
@@ -13,12 +22,6 @@ use harmony::{
#[tokio::main]
async fn main() {
env_logger::init();
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
let application = Arc::new(RustWebapp {
name: "harmony-example-rust-webapp".to_string(),
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
@@ -26,18 +29,34 @@ async fn main() {
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 {
features: vec![
Box::new(ContinuousDelivery {
application: application.clone(),
}), // TODO add monitoring, backups, multisite ha, etc
Box::new(Monitoring {
//Box::new(ContinuousDelivery {
// application: application.clone(),
//}),
Box::new(PrometheusMonitoring {
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,
};
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
maestro.register_all(vec![Box::new(app)]);
harmony_cli::init(maestro, None).await.unwrap();
}

View File

@@ -58,11 +58,3 @@ futures-util = "0.3.31"
tokio-util = "0.7.15"
strum = { version = "0.27.1", features = ["derive"] }
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 serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub data_dir: PathBuf,
pub registry_url: String,
pub registry_project: String,
pub dry_run: bool,
pub run_upgrades: bool,
}
impl Default for Config {
fn default() -> Self {
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)
}
lazy_static! {
pub static ref HARMONY_DATA_DIR: PathBuf = directories::BaseDirs::new()
.unwrap()
.data_dir()
.join("harmony");
pub static ref REGISTRY_URL: String =
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());
pub static ref DRY_RUN: bool =
std::env::var("HARMONY_DRY_RUN").map_or(true, |value| value.parse().unwrap_or(true));
}

View File

@@ -1,15 +1,11 @@
use async_trait::async_trait;
use harmony_macros::ip;
use harmony_types::net::MacAddress;
use log::error;
use log::info;
use crate::config::Config;
use crate::executors::ExecutorError;
use crate::interpret::InterpretError;
use crate::interpret::Outcome;
use crate::inventory::Inventory;
use crate::topology::upgradeable::Upgradeable;
use super::DHCPStaticEntry;
use super::DhcpServer;
@@ -29,12 +25,9 @@ use super::TftpServer;
use super::Topology;
use super::Url;
use super::k8s::K8sClient;
use std::fmt::Debug;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Clone, Debug)]
#[derive(Debug, Clone)]
pub struct HAClusterTopology {
pub domain_name: String,
pub router: Arc<dyn Router>,
@@ -56,15 +49,9 @@ impl Topology for HAClusterTopology {
"HAClusterTopology"
}
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."
);
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";
impl Router for DummyInfra {
@@ -437,12 +417,3 @@ impl DnsServer for DummyInfra {
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 similar::{DiffableStr, TextDiff};
use crate::config::Config as HarmonyConfig;
#[derive(new, Clone)]
pub struct K8sClient {
client: Client,
@@ -156,9 +154,7 @@ impl K8sClient {
.as_ref()
.expect("K8s Resource should have a name");
let config = HarmonyConfig::load().expect("couldn't load config");
if config.dry_run {
if *crate::config::DRY_RUN {
match api.get(name).await {
Ok(current) => {
trace!("Received current value {current:#?}");
@@ -264,33 +260,17 @@ impl K8sClient {
) -> Result<(), Error> {
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 namespace = obj
.metadata
.namespace
.as_ref()
.expect("YAML must have a namespace");
let api_version = yaml
.get("apiVersion")
.expect("couldn't get apiVersion from YAML")
.as_str()
.expect("couldn't get apiVersion as str");
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"),
};
// 4. Define the API resource type using the GVK from the object.
// The plural name 'applications' is taken from your CRD definition.
error!("This only supports argocd application harcoded, very rrrong");
let gvk = GroupVersionKind::gvk("argoproj.io", "v1alpha1", "Application");
let api_resource = ApiResource::from_gvk_with_plural(&gvk, "applications");
// 5. Create a dynamic API client for this resource type.
let api: Api<DynamicObject> =

View File

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

View File

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

View File

@@ -2,15 +2,9 @@ use std::{net::Ipv4Addr, str::FromStr, sync::Arc};
use async_trait::async_trait;
use harmony_types::net::MacAddress;
use log::debug;
use serde::Serialize;
use crate::{
executors::ExecutorError,
interpret::InterpretError,
inventory::Inventory,
topology::{Topology, upgradeable::Upgradeable},
};
use crate::executors::ExecutorError;
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 name: String,
}

View File

@@ -1,3 +1,5 @@
use std::any::Any;
use async_trait::async_trait;
use log::debug;
@@ -9,7 +11,7 @@ use crate::{
};
#[async_trait]
pub trait AlertSender: Send + Sync + std::fmt::Debug {
pub trait AlertSender: Any + Send + Sync + std::fmt::Debug {
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;
use std::sync::Arc;
use async_trait::async_trait;
pub use management::*;
use opnsense_config_xml::Host;
use tokio::sync::RwLock;
use crate::{
executors::ExecutorError,
interpret::InterpretError,
inventory::Inventory,
modules::opnsense::OPNSenseLaunchUpgrade,
score::Score,
topology::{IpAddress, LogicalHost, Topology, upgradeable::Upgradeable},
topology::{IpAddress, LogicalHost},
};
#[derive(Debug, Clone)]
@@ -54,17 +49,3 @@ impl OPNSenseFirewall {
.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 serde::Serialize;
use serde_with::skip_serializing_none;
use serde_yaml::Value;
use url::Url;
use crate::modules::application::features::CDApplicationConfig;
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Helm {
@@ -25,18 +27,13 @@ pub struct Helm {
pub namespace: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
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,
// 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 repo_url: Url,
pub target_revision: Option<String>,
pub chart: String,
pub helm: Helm,
pub path: String,
}
#[derive(Clone, Debug, Serialize)]
@@ -70,7 +67,6 @@ pub struct SyncPolicy {
pub retry: Retry,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ArgoApplication {
@@ -89,7 +85,7 @@ impl Default for ArgoApplication {
namespace: Default::default(),
project: Default::default(),
source: Source {
repo_url: "http://asdf".to_string(),
repo_url: Url::parse("http://asdf").expect("Couldn't parse to URL"),
target_revision: None,
chart: "".to_string(),
helm: Helm {
@@ -108,7 +104,6 @@ impl Default for ArgoApplication {
api_versions: vec![],
namespace: None,
},
path: "".to_string(),
},
sync_policy: SyncPolicy {
automated: Automated {
@@ -138,10 +133,10 @@ impl From<CDApplicationConfig> for ArgoApplication {
namespace: Some(value.namespace),
project: "default".to_string(),
source: Source {
repo_url: value.helm_chart_repo_url,
target_revision: Some(value.version.to_string()),
chart: value.helm_chart_name.clone(),
path: value.helm_chart_name,
repo_url: Url::parse(value.helm_chart_repo_url.to_string().as_str())
.expect("couldn't convert to URL"),
target_revision: None,
chart: value.helm_chart_name,
helm: Helm {
pass_credentials: None,
parameters: vec![],
@@ -150,7 +145,7 @@ impl From<CDApplicationConfig> for ArgoApplication {
value_files: vec![],
ignore_missing_value_files: None,
values: None,
values_object: value.values_overrides,
values_object: Some(value.values_overrides),
skip_crds: None,
skip_schema_validation: None,
version: None,
@@ -218,7 +213,7 @@ spec:
let mut yaml_value: Value =
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")
.expect("couldn't get spec from yaml")
.as_mapping_mut()
@@ -257,7 +252,7 @@ spec:
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use url::Url;
use crate::modules::application::features::{
ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy,
@@ -270,7 +265,7 @@ mod tests {
namespace: Some("test-ns".to_string()),
project: "test-project".to_string(),
source: Source {
repo_url: "http://test".to_string(),
repo_url: Url::parse("http://test").unwrap(),
target_revision: None,
chart: "test-chart".to_string(),
helm: Helm {
@@ -289,7 +284,6 @@ mod tests {
api_versions: vec![],
namespace: None,
},
path: "".to_string(),
},
sync_policy: SyncPolicy {
automated: Automated {
@@ -321,15 +315,24 @@ spec:
server: https://kubernetes.default.svc
namespace: test-ns
source:
repoURL: http://test
repoUrl: http://test/
targetRevision: null
chart: test-chart
helm:
passCredentials: null
parameters: []
fileParameters: []
releaseName: test-release-neame
valueFiles: []
ignoreMissingValueFiles: null
values: null
valuesObject: null
skipCrds: null
skipSchemaValidation: null
version: null
kubeVersion: null
apiVersions: []
path: ''
namespace: null
syncPolicy:
automated:
prune: false

View File

@@ -6,15 +6,18 @@ use serde_yaml::Value;
use tempfile::NamedTempFile;
use crate::{
config::Config,
config::HARMONY_DATA_DIR,
data::Version,
inventory::Inventory,
modules::application::{
Application, ApplicationFeature, HelmPackage, OCICompliant,
features::{ArgoApplication, ArgoHelmScore},
modules::{
application::{
Application, ApplicationFeature, HelmPackage, OCICompliant,
features::{ArgoApplication, ArgoHelmScore},
},
helm::chart::HelmChartScore,
},
score::Score,
topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology},
topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, Url},
};
/// ContinuousDelivery in Harmony provides this functionality :
@@ -56,14 +59,12 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
chart_url: String,
image_name: String,
) -> Result<(), String> {
let config = Config::load().expect("couldn't load config");
error!(
"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");
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 ---
info!(
"Importing image '{}' into k3d cluster 'harmony'",
@@ -160,7 +161,6 @@ impl<
let helm_chart = self.application.build_push_helm_package(&image).await?;
info!("Pushed new helm chart {helm_chart}");
error!("TODO Make building image configurable/skippable");
let image = self.application.build_push_oci_image().await?;
info!("Pushed new docker image {image}");
@@ -187,14 +187,14 @@ impl<
info!("Deploying to target {target:?}");
let score = ArgoHelmScore {
namespace: "harmonydemo-staging".to_string(),
openshift: false,
openshift: true,
domain: "argo.harmonydemo.apps.st.mcd".to_string(),
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(),
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(),
values_overrides: None,
values_overrides: Value::Null,
name: "harmony-demo-rust-webapp".to_string(),
namespace: "harmonydemo-staging".to_string(),
})],
@@ -206,7 +206,14 @@ impl<
.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 {
"ContinuousDelivery".to_string()
@@ -217,9 +224,9 @@ impl<
/// more CD systems
pub struct CDApplicationConfig {
pub version: Version,
pub helm_chart_repo_url: String,
pub helm_chart_repo_url: Url,
pub helm_chart_name: String,
pub values_overrides: Option<Value>,
pub values_overrides: Value,
pub name: String,
pub namespace: String,
}

View File

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

View File

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

View File

@@ -4,17 +4,13 @@ use std::process;
use std::sync::Arc;
use async_trait::async_trait;
use bollard::query_parameters::PushImageOptionsBuilder;
use bollard::{Docker, body_full};
use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt;
use log::{debug, error, info};
use serde::Serialize;
use tar::Archive;
use crate::config::Config;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::{
score::Score,
topology::{Topology, Url},
@@ -112,7 +108,6 @@ impl OCICompliant for RustWebapp {
// 1. Build the local image by calling the synchronous helper function.
let local_image_name = self.local_image_name();
self.build_docker_image(&local_image_name)
.await
.map_err(|e| format!("Failed to build Docker image: {}", e))?;
info!(
"Successfully built local Docker image: {}",
@@ -122,7 +117,6 @@ impl OCICompliant for RustWebapp {
let remote_image_name = self.image_name();
// 2. Push the image to the registry.
self.push_docker_image(&local_image_name, &remote_image_name)
.await
.map_err(|e| format!("Failed to push Docker image: {}", e))?;
info!("Successfully pushed Docker image to: {}", remote_image_name);
@@ -134,12 +128,10 @@ impl OCICompliant for RustWebapp {
}
fn image_name(&self) -> String {
let config = Config::load().expect("couldn't load config");
format!(
"{}/{}/{}",
config.registry_url,
config.registry_project,
*REGISTRY_URL,
*REGISTRY_PROJECT,
&self.local_image_name()
)
}
@@ -161,68 +153,66 @@ impl RustWebapp {
}
/// Builds the Docker image using the generated Dockerfile.
pub async fn build_docker_image(
pub fn build_docker_image(
&self,
image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> {
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();
let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
.dockerfile("Dockerfile.harmony")
.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())),
info!(
"Building Docker image with file {} from root {}",
dockerfile_path.to_string_lossy(),
self.project_root.to_string_lossy()
);
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 {
println!("Message: {msg:?}");
}
self.check_output(&output, "Failed to build Docker image")?;
Ok(image_name.to_string())
}
/// Tags and pushes a Docker image to the configured remote registry.
async fn push_docker_image(
fn push_docker_image(
&self,
image_name: &str,
full_tag: &str,
) -> Result<String, Box<dyn std::error::Error>> {
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);
let mut push_image_stream =
docker.push_image(full_tag, Some(PushImageOptionsBuilder::new().build()), None);
while let Some(msg) = push_image_stream.next().await {
println!("Message: {msg:?}");
}
// Push the image.
let output = process::Command::new("docker")
.args(["push", &full_tag])
.spawn()?
.wait_with_output()?;
self.check_output(&output, "Pushing docker image failed")?;
debug!(
"docker push output: stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok(full_tag.to_string())
}
@@ -359,11 +349,7 @@ impl RustWebapp {
image_url: &str,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let chart_name = format!("{}-chart", self.name);
let chart_dir = self
.project_root
.join(".harmony_generated")
.join("helm")
.join(&chart_name);
let chart_dir = self.project_root.join("helm").join(&chart_name);
let templates_dir = chart_dir.join("templates");
fs::create_dir_all(&templates_dir)?;
@@ -430,7 +416,7 @@ ingress:
Expand the name of the chart.
*/}}
{{- define "chart.name" -}}
{{- default .Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- 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).
*/}}
{{- define "chart.fullname" -}}
{{- $name := default .Chart.Name $.Values.nameOverride }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
"#;
@@ -451,12 +437,12 @@ kind: Service
metadata:
name: {{ include "chart.fullname" . }}
spec:
type: {{ $.Values.service.type }}
type: {{ .Values.service.type }}
ports:
- name: main
port: {{ $.Values.service.port | default 3000 }}
targetPort: {{ $.Values.service.port | default 3000 }}
- port: {{ .Values.service.port }}
targetPort: 3000
protocol: TCP
name: http
selector:
app: {{ include "chart.name" . }}
"#;
@@ -469,7 +455,7 @@ kind: Deployment
metadata:
name: {{ include "chart.fullname" . }}
spec:
replicas: {{ $.Values.replicaCount }}
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "chart.name" . }}
@@ -480,28 +466,28 @@ spec:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ $.Values.image.pullPolicy }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: main
containerPort: {{ $.Values.service.port | default 3000 }}
- name: http
containerPort: 3000
protocol: TCP
"#;
fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
// Create templates/ingress.yaml
let ingress_yaml = r#"
{{- if $.Values.ingress.enabled -}}
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "chart.fullname" . }}
annotations:
{{- toYaml $.Values.ingress.annotations | nindent 4 }}
{{- toYaml .Values.ingress.annotations | nindent 4 }}
spec:
{{- if $.Values.ingress.tls }}
{{- if .Values.ingress.tls }}
tls:
{{- range $.Values.ingress.tls }}
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
@@ -510,7 +496,7 @@ spec:
{{- end }}
{{- end }}
rules:
{{- range $.Values.ingress.hosts }}
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
@@ -521,7 +507,7 @@ spec:
service:
name: {{ include "chart.fullname" $ }}
port:
number: {{ $.Values.service.port | default 3000 }}
number: 3000
{{- end }}
{{- end }}
{{- end }}
@@ -540,15 +526,11 @@ spec:
info!(
"Launching `helm package {}` cli with CWD {}",
chart_dirname.to_string_lossy(),
&self
.project_root
.join(".harmony_generated")
.join("helm")
.to_string_lossy()
&self.project_root.join("helm").to_string_lossy()
);
let output = process::Command::new("helm")
.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()?;
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.
Ok(self
.project_root
.join(".harmony_generated")
.join("helm")
.join(tgz_name))
Ok(self.project_root.join("helm").join(tgz_name))
}
/// Pushes a packaged Helm chart to an OCI registry.
@@ -577,11 +555,9 @@ spec:
&self,
packaged_chart_path: &PathBuf,
) -> 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
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);
info!(

View File

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

View File

@@ -10,25 +10,14 @@ use crate::{
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)]
pub struct StaticFilesHttpScore {
pub struct HttpScore {
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>> {
Box::new(StaticFilesHttpInterpret::new(self.clone()))
Box::new(HttpInterpret::new(self.clone()))
}
fn name(&self) -> String {
@@ -37,12 +26,12 @@ impl<T: Topology + HttpServer> Score<T> for StaticFilesHttpScore {
}
#[derive(Debug, new, Clone)]
pub struct StaticFilesHttpInterpret {
score: StaticFilesHttpScore,
pub struct HttpInterpret {
score: HttpScore,
}
#[async_trait]
impl<T: Topology + HttpServer> Interpret<T> for StaticFilesHttpInterpret {
impl<T: Topology + HttpServer> Interpret<T> for HttpInterpret {
async fn execute(
&self,
_inventory: &Inventory,

View File

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

View File

@@ -14,7 +14,7 @@ use async_trait::async_trait;
use log::{debug, info};
use serde::Serialize;
use crate::config::Config as HarmonyConfig;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::modules::k8s::ingress::K8sIngressScore;
use crate::topology::HelmCommand;
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>> {
let config = HarmonyConfig::load().expect("couldn't load config");
let full_tag = format!(
"{}/{}/{}",
config.registry_url, config.registry_project, &image_name
);
let full_tag = format!("{}/{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT, &image_name);
let output = std::process::Command::new("docker")
.args(["tag", image_name, &full_tag])
.output()?;

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
use non_blank_string_rs::NonBlankString;
use std::str::FromStr;
use non_blank_string_rs::NonBlankString;
use crate::modules::helm::chart::HelmChartScore;
pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore {
@@ -8,10 +7,46 @@ pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore {
r#"
rbac:
namespaced: true
sidecar:
dashboards:
enabled: true
"#
datasources:
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 {
@@ -20,9 +55,10 @@ sidecar:
chart_name: NonBlankString::from_str("oci://ghcr.io/grafana/helm-charts/grafana").unwrap(),
chart_version: None,
values_overrides: None,
values_yaml: Some(values.to_string()),
values_yaml: Some(values),
create_namespace: true,
install_only: true,
install_only: false,
repository: None,
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ use std::str::FromStr;
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!(
r#"
replicaCount: 1
@@ -28,12 +28,12 @@ service:
port: 80
ingress:
enabled: true
enabled: false
# annotations:
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: {host}
- host: ntfy.host.com
paths:
- path: /
pathType: ImplementationSpecific

View File

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

View File

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

View File

@@ -1,37 +1,145 @@
use std::collections::BTreeMap;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use log::debug;
use non_blank_string_rs::NonBlankString;
use serde_yaml::{Mapping, Value};
use crate::modules::{
helm::chart::HelmChartScore, monitoring::prometheus::prometheus_config::PrometheusConfig,
use crate::modules::helm::chart::HelmChartScore;
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 ns = config.namespace.clone().unwrap();
let values = format!(
r#"
rbac:
create: true
kube-state-metrics:
enabled: false
nodeExporter:
enabled: false
alertmanager:
enabled: false
pushgateway:
enabled: false
server:
serviceAccount:
create: false
rbac:
create: true
fullnameOverride: prometheus-{ns}
"#
let rbac_config = RbacConfig { create: false };
let ksm_config = KubeStateMetricsConfig {
enabled: true,
rbac: KsmRbacConfig {
use_cluster_role: false,
},
prometheus: Prometheus {
monitor: Monitor { enabled: true },
},
};
let mut selector_labels = BTreeMap::new();
selector_labels.insert("kubernetes.io/metadata.name".to_string(), ns.clone());
let mut kube_state_metrics_labels = BTreeMap::new();
kube_state_metrics_labels.insert(
"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 {
namespace: Some(NonBlankString::from_str(&config.namespace.clone().unwrap()).unwrap()),
namespace: Some(NonBlankString::from_str(&ns).unwrap()),
release_name: NonBlankString::from_str("prometheus").unwrap(),
chart_name: NonBlankString::from_str(
"oci://ghcr.io/prometheus-community/charts/prometheus",
@@ -39,7 +147,7 @@ fullnameOverride: prometheus-{ns}
.unwrap(),
chart_version: None,
values_overrides: None,
values_yaml: Some(values.to_string()),
values_yaml: Some(values_yaml),
create_namespace: true,
install_only: true,
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,
topology::{
HelmCommand, Topology,
HelmCommand, K8sclient, Topology,
installable::Installable,
oberservability::monitoring::{AlertReceiver, AlertRule, AlertSender},
tenant::TenantManager,
@@ -22,12 +22,12 @@ use crate::{
};
use super::{
helm::prometheus_helm::prometheus_helm_chart_score, prometheus_config::PrometheusConfig,
helm::prometheus_helm::prometheus_helm_chart_score, prometheus_config::HelmPrometheusConfig,
};
#[derive(Debug)]
pub struct Prometheus {
pub config: Arc<Mutex<PrometheusConfig>>,
pub config: Arc<Mutex<HelmPrometheusConfig>>,
}
#[async_trait]
@@ -40,18 +40,17 @@ impl AlertSender for Prometheus {
impl Prometheus {
pub fn new() -> 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) {
let ns = topology
.get_tenant_config()
.await
.map(|cfg| cfg.name.clone())
.unwrap_or_else(|| "monitoring".to_string());
if let Some(cfg) = topology.get_tenant_config().await {
debug!("Overriding namespace with tenant config: {}", cfg.name);
self.config.lock().unwrap().namespace = Some(cfg.name.clone());
} else {
debug!("No tenant config found; keeping existing namespace.");
}
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(

View File

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

View File

@@ -1 +1,2 @@
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_file_override: Option<MaybeString>,
pub adv_dhcp_config_file_override_path: Option<MaybeString>,
pub mtu: Option<u32>,
}
#[cfg(test)]

View File

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