Compare commits

...

10 Commits

36 changed files with 659 additions and 236 deletions

1
.gitignore vendored
View File

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

153
Cargo.lock generated
View File

@@ -219,6 +219,15 @@ 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"
@@ -409,6 +418,12 @@ 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"
@@ -1343,6 +1358,7 @@ dependencies = [
"cidr", "cidr",
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli",
"harmony_macros", "harmony_macros",
"harmony_tui", "harmony_tui",
"harmony_types", "harmony_types",
@@ -1355,6 +1371,7 @@ 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",
@@ -1427,6 +1444,31 @@ 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"
@@ -1726,6 +1768,8 @@ 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",
@@ -1735,6 +1779,7 @@ dependencies = [
"dyn-clone", "dyn-clone",
"email_address", "email_address",
"env_logger", "env_logger",
"figment",
"fqdn", "fqdn",
"futures-util", "futures-util",
"harmony_macros", "harmony_macros",
@@ -1752,6 +1797,7 @@ 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",
@@ -1760,9 +1806,11 @@ 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",
@@ -2397,6 +2445,12 @@ 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"
@@ -2726,6 +2780,7 @@ checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"libc", "libc",
"redox_syscall",
] ]
[[package]] [[package]]
@@ -3217,6 +3272,29 @@ 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"
@@ -3479,6 +3557,19 @@ 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"
@@ -4080,6 +4171,18 @@ 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"
@@ -4280,22 +4383,36 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.13.0" version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
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", "schemars 0.9.0",
"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"
@@ -4668,6 +4785,17 @@ 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"
@@ -5084,6 +5212,15 @@ 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"
@@ -5713,6 +5850,16 @@ 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,3 +52,8 @@ convert_case = "0.8"
chrono = "0.4" chrono = "0.4"
similar = "2" similar = "2"
uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] } uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] }
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::HttpScore, http::StaticFilesHttpScore,
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 = HttpScore::new(Url::LocalFolder( let http_score = StaticFilesHttpScore::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,6 +14,7 @@ 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,3 +16,4 @@ 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::HttpScore, http::StaticFilesHttpScore,
okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore}, okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore},
opnsense::OPNsenseShellCommandScore, opnsense::{OPNSenseLaunchUpgrade, OPNsenseShellCommandScore},
tftp::TftpScore, tftp::TftpScore,
}, },
topology::{LogicalHost, UnmanagedRouter, Url}, topology::{LogicalHost, UnmanagedRouter, Url},
@@ -22,8 +22,10 @@ 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.5.229"), ip: ip!("192.168.122.106"),
name: String::from("opnsense-1"), name: String::from("opnsense-1"),
}; };
@@ -81,7 +83,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 = HttpScore::new(Url::LocalFolder( let http_score = StaticFilesHttpScore::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();
@@ -95,9 +97,12 @@ 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_tui::init(maestro).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@@ -12,3 +12,4 @@ 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

@@ -13,6 +13,12 @@ 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()),
@@ -24,17 +30,14 @@ async fn main() {
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 {
application: application.clone(),
}), }),
Box::new(Monitoring {}),
// 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,3 +58,11 @@ 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,15 +1,66 @@
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;
lazy_static! { #[derive(Debug, Deserialize, Serialize)]
pub static ref HARMONY_DATA_DIR: PathBuf = directories::BaseDirs::new() pub struct Config {
.unwrap() pub data_dir: PathBuf,
.data_dir() pub registry_url: String,
.join("harmony"); pub registry_project: String,
pub static ref REGISTRY_URL: String = pub dry_run: bool,
std::env::var("HARMONY_REGISTRY_URL").unwrap_or_else(|_| "hub.nationtech.io".to_string()); pub run_upgrades: bool,
pub static ref REGISTRY_PROJECT: String = }
std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string());
pub static ref DRY_RUN: bool = impl Default for Config {
std::env::var("HARMONY_DRY_RUN").map_or(true, |value| value.parse().unwrap_or(true)); 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)
}
} }

View File

@@ -1,11 +1,15 @@
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;
@@ -25,9 +29,12 @@ 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(Debug, Clone)] #[derive(Clone, Debug)]
pub struct HAClusterTopology { pub struct HAClusterTopology {
pub domain_name: String, pub domain_name: String,
pub router: Arc<dyn Router>, pub router: Arc<dyn Router>,
@@ -49,9 +56,15 @@ impl Topology for HAClusterTopology {
"HAClusterTopology" "HAClusterTopology"
} }
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> { async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
todo!( error!(
"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()))
} }
} }
@@ -251,6 +264,13 @@ 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 {
@@ -417,3 +437,12 @@ 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,6 +20,8 @@ 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,
@@ -154,7 +156,9 @@ impl K8sClient {
.as_ref() .as_ref()
.expect("K8s Resource should have a name"); .expect("K8s Resource should have a name");
if *crate::config::DRY_RUN { let config = HarmonyConfig::load().expect("couldn't load config");
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:#?}");
@@ -260,17 +264,33 @@ 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");
// 4. Define the API resource type using the GVK from the object. let api_version = yaml
// The plural name 'applications' is taken from your CRD definition. .get("apiVersion")
error!("This only supports argocd application harcoded, very rrrong"); .expect("couldn't get apiVersion from YAML")
let gvk = GroupVersionKind::gvk("argoproj.io", "v1alpha1", "Application"); .as_str()
let api_resource = ApiResource::from_gvk_with_plural(&gvk, "applications"); .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"),
};
// 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,9 +1,10 @@
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::Serialize; use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use crate::{ use crate::{
@@ -219,7 +220,7 @@ impl K8sAnywhereTopology {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize)]
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
@@ -246,25 +247,29 @@ pub struct K8sAnywhereConfig {
/// ///
/// default: true /// default: true
pub use_local_k3d: bool, pub use_local_k3d: bool,
pub harmony_profile: String, 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,
}
}
} }
impl K8sAnywhereConfig { impl K8sAnywhereConfig {
fn from_env() -> Self { fn from_env() -> Self {
Self { Figment::new()
kubeconfig: std::env::var("KUBECONFIG").ok().map(|v| v.to_string()), .merge(Env::prefixed("HARMONY_"))
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG") .merge(Env::raw().only(&["KUBECONFIG"]))
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), .extract()
autoinstall: std::env::var("HARMONY_AUTOINSTALL") .expect("couldn't load config from env")
.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)),
}
} }
} }
@@ -304,7 +309,7 @@ impl MultiTargetTopology for K8sAnywhereTopology {
return DeploymentTarget::LocalDev; return DeploymentTarget::LocalDev;
} }
match self.config.harmony_profile.to_lowercase().as_str() { match self.config.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,6 +6,7 @@ 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,9 +2,15 @@ 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::executors::ExecutorError; use crate::{
executors::ExecutorError,
interpret::InterpretError,
inventory::Inventory,
topology::{Topology, upgradeable::Upgradeable},
};
use super::{IpAddress, LogicalHost, k8s::K8sClient}; use super::{IpAddress, LogicalHost, k8s::K8sClient};
@@ -38,6 +44,15 @@ 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

@@ -0,0 +1,8 @@
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,13 +7,18 @@ 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,
topology::{IpAddress, LogicalHost}, interpret::InterpretError,
inventory::Inventory,
modules::opnsense::OPNSenseLaunchUpgrade,
score::Score,
topology::{IpAddress, LogicalHost, Topology, upgradeable::Upgradeable},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -49,3 +54,17 @@ 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,13 +1,11 @@
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 {
@@ -27,13 +25,18 @@ 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 {
pub repo_url: Url, // 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 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)]
@@ -67,6 +70,7 @@ 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 {
@@ -85,7 +89,7 @@ impl Default for ArgoApplication {
namespace: Default::default(), namespace: Default::default(),
project: Default::default(), project: Default::default(),
source: Source { source: Source {
repo_url: Url::parse("http://asdf").expect("Couldn't parse to URL"), repo_url: "http://asdf".to_string(),
target_revision: None, target_revision: None,
chart: "".to_string(), chart: "".to_string(),
helm: Helm { helm: Helm {
@@ -104,6 +108,7 @@ 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 {
@@ -133,10 +138,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: Url::parse(value.helm_chart_repo_url.to_string().as_str()) repo_url: value.helm_chart_repo_url,
.expect("couldn't convert to URL"), target_revision: Some(value.version.to_string()),
target_revision: None, chart: value.helm_chart_name.clone(),
chart: value.helm_chart_name, path: value.helm_chart_name,
helm: Helm { helm: Helm {
pass_credentials: None, pass_credentials: None,
parameters: vec![], parameters: vec![],
@@ -145,7 +150,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: Some(value.values_overrides), values_object: value.values_overrides,
skip_crds: None, skip_crds: None,
skip_schema_validation: None, skip_schema_validation: None,
version: None, version: None,
@@ -213,7 +218,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 mut spec = yaml_value let 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()
@@ -252,7 +257,7 @@ spec:
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use url::Url; use pretty_assertions::assert_eq;
use crate::modules::application::features::{ use crate::modules::application::features::{
ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy, ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy,
@@ -265,7 +270,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: Url::parse("http://test").unwrap(), repo_url: "http://test".to_string(),
target_revision: None, target_revision: None,
chart: "test-chart".to_string(), chart: "test-chart".to_string(),
helm: Helm { helm: Helm {
@@ -284,6 +289,7 @@ 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 {
@@ -315,24 +321,15 @@ 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: []
namespace: null path: ''
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: false

View File

@@ -6,18 +6,15 @@ use serde_yaml::Value;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::{ use crate::{
config::HARMONY_DATA_DIR, config::Config,
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::{ modules::application::{
application::{ Application, ApplicationFeature, HelmPackage, OCICompliant,
Application, ApplicationFeature, HelmPackage, OCICompliant, features::{ArgoApplication, ArgoHelmScore},
features::{ArgoApplication, ArgoHelmScore},
},
helm::chart::HelmChartScore,
}, },
score::Score, score::Score,
topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, Url}, topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology},
}; };
/// ContinuousDelivery in Harmony provides this functionality : /// ContinuousDelivery in Harmony provides this functionality :
@@ -59,12 +56,14 @@ 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 = (*HARMONY_DATA_DIR).join("k3d").join("k3d"); let k3d_bin_path = config.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'",
@@ -161,6 +160,7 @@ 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: true, openshift: false,
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/harmony-example-rust-webapp-chart --version 0.1.0 // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
version: Version::from("0.1.0").unwrap(), version: Version::from("0.1.0").unwrap(),
helm_chart_repo_url: Url::Url(url::Url::parse("oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart/harmony-example-rust-webapp-chart").unwrap()), helm_chart_repo_url: "hub.nationtech.io/harmony".to_string(),
helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), helm_chart_name: "harmony-example-rust-webapp-chart".to_string(),
values_overrides: Value::Null, values_overrides: None,
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,14 +206,7 @@ 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()
@@ -224,9 +217,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: Url, pub helm_chart_repo_url: String,
pub helm_chart_name: String, pub helm_chart_name: String,
pub values_overrides: Value, pub values_overrides: Option<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 k8s_openapi::Resource; use log::error;
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,6 +50,7 @@ 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)
@@ -645,7 +646,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: false enabled: true
# -- 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,36 +1,99 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use base64::{Engine as _, engine::general_purpose};
use log::{debug, info};
use crate::{ use crate::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
application::{Application, ApplicationFeature}, application::{ApplicationFeature, OCICompliant},
monitoring::{ monitoring::{
application_monitoring::k8s_application_monitoring_score::ApplicationPrometheusMonitoringScore, alert_channel::webhook_receiver::WebhookReceiver,
kube_prometheus::{ kube_prometheus::{
helm_prometheus_alert_score::HelmPrometheusAlertingScore, helm_prometheus_alert_score::HelmPrometheusAlertingScore,
types::{NamespaceSelector, ServiceMonitor}, types::{NamespaceSelector, ServiceMonitor},
}, },
ntfy::ntfy::NtfyScore,
}, },
}, },
score::Score, score::Score,
topology::{HelmCommand, Topology, tenant::TenantManager}, topology::{HelmCommand, K8sclient, Topology, Url, tenant::TenantManager},
}; };
#[derive(Debug, Default, Clone)] #[derive(Debug, Clone)]
pub struct Monitoring {} pub struct Monitoring {
pub application: Arc<dyn OCICompliant>,
}
#[async_trait] #[async_trait]
impl<T: Topology + HelmCommand + 'static + TenantManager> ApplicationFeature<T> for Monitoring { impl<T: Topology + HelmCommand + K8sclient + 'static + TenantManager> ApplicationFeature<T>
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 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.namespace_selector = Some(NamespaceSelector { service_monitor.namespace_selector = Some(NamespaceSelector {
any: true, any: true,
match_names: vec![], match_names: vec![],
}); });
let alerting_score = ApplicationPrometheusMonitoringScore {
receivers: vec![], 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![], rules: vec![],
service_monitors: vec![service_monitor], service_monitors: vec![service_monitor],
}; };

View File

@@ -59,9 +59,7 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
} }
}; };
} }
todo!( Ok(Outcome::success("successfully created app".to_string()))
"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,13 +4,17 @@ 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::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::config::Config;
use crate::{ use crate::{
score::Score, score::Score,
topology::{Topology, Url}, topology::{Topology, Url},
@@ -108,6 +112,7 @@ 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: {}",
@@ -117,6 +122,7 @@ 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);
@@ -128,10 +134,12 @@ 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!(
"{}/{}/{}", "{}/{}/{}",
*REGISTRY_URL, config.registry_url,
*REGISTRY_PROJECT, config.registry_project,
&self.local_image_name() &self.local_image_name()
) )
} }
@@ -153,66 +161,68 @@ impl RustWebapp {
} }
/// Builds the Docker image using the generated Dockerfile. /// Builds the Docker image using the generated Dockerfile.
pub fn build_docker_image( pub async 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()?;
info!( let docker = Docker::connect_with_socket_defaults().unwrap();
"Building Docker image with file {} from root {}",
dockerfile_path.to_string_lossy(), let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
self.project_root.to_string_lossy() .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())),
); );
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()?;
self.check_output(&output, "Failed to build Docker image")?; while let Some(msg) = image_build_stream.next().await {
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.
fn push_docker_image( async 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}");
// Tag the image for the remote registry. let docker = Docker::connect_with_socket_defaults().unwrap();
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)
);
// Push the image. // let push_options = PushImageOptionsBuilder::new().tag(tag);
let output = process::Command::new("docker")
.args(["push", &full_tag]) let mut push_image_stream =
.spawn()? docker.push_image(full_tag, Some(PushImageOptionsBuilder::new().build()), None);
.wait_with_output()?;
self.check_output(&output, "Pushing docker image failed")?; while let Some(msg) = push_image_stream.next().await {
debug!( println!("Message: {msg:?}");
"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())
} }
@@ -349,7 +359,11 @@ 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.project_root.join("helm").join(&chart_name); let chart_dir = self
.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)?;
@@ -416,7 +430,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 }}
{{/* {{/*
@@ -424,7 +438,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 }}
"#; "#;
@@ -437,12 +451,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:
- port: {{ .Values.service.port }} - name: main
targetPort: 3000 port: {{ $.Values.service.port | default 3000 }}
targetPort: {{ $.Values.service.port | default 3000 }}
protocol: TCP protocol: TCP
name: http
selector: selector:
app: {{ include "chart.name" . }} app: {{ include "chart.name" . }}
"#; "#;
@@ -455,7 +469,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" . }}
@@ -466,28 +480,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: http - name: main
containerPort: 3000 containerPort: {{ $.Values.service.port | default 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 }}
@@ -496,7 +510,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:
@@ -507,7 +521,7 @@ spec:
service: service:
name: {{ include "chart.fullname" $ }} name: {{ include "chart.fullname" $ }}
port: port:
number: 3000 number: {{ $.Values.service.port | default 3000 }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
@@ -526,11 +540,15 @@ 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.project_root.join("helm").to_string_lossy() &self
.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("helm")) // Run package from the parent dir .current_dir(&self.project_root.join(".harmony_generated").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")?;
@@ -547,7 +565,11 @@ 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.project_root.join("helm").join(tgz_name)) Ok(self
.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.
@@ -555,9 +577,11 @@ 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://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT); let oci_push_url = format!("oci://{}/{}", config.registry_url, config.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

@@ -10,14 +10,25 @@ 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 HttpScore { pub struct StaticFilesHttpScore {
files_to_serve: Url, files_to_serve: Url,
} }
impl<T: Topology + HttpServer> Score<T> for HttpScore { impl<T: Topology + HttpServer> Score<T> for StaticFilesHttpScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(HttpInterpret::new(self.clone())) Box::new(StaticFilesHttpInterpret::new(self.clone()))
} }
fn name(&self) -> String { fn name(&self) -> String {
@@ -26,12 +37,12 @@ impl<T: Topology + HttpServer> Score<T> for HttpScore {
} }
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone)]
pub struct HttpInterpret { pub struct StaticFilesHttpInterpret {
score: HttpScore, score: StaticFilesHttpScore,
} }
#[async_trait] #[async_trait]
impl<T: Topology + HttpServer> Interpret<T> for HttpInterpret { impl<T: Topology + HttpServer> Interpret<T> for StaticFilesHttpInterpret {
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::HARMONY_DATA_DIR, config::Config,
data::{Id, Version}, data::{Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
@@ -21,8 +21,10 @@ 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: HARMONY_DATA_DIR.join("k3d"), installation_path: config.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::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::config::Config as HarmonyConfig;
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,7 +355,12 @@ 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 full_tag = format!("{}/{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT, &image_name); let config = HarmonyConfig::load().expect("couldn't load config");
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

@@ -24,7 +24,10 @@ 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 config = Arc::new(Mutex::new(PrometheusConfig::new())); let mut prom_config = PrometheusConfig::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")

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: false, kubernetes_api_server: true,
kubelet: true, kubelet: true,
kube_controller_manager: false, kube_controller_manager: true,
kube_etcd: false, kube_etcd: true,
kube_proxy: false, kube_proxy: true,
kube_state_metrics: true, kube_state_metrics: true,
prometheus_operator: true, prometheus_operator: true,
core_dns: false, core_dns: true,
kube_scheduler: false, kube_scheduler: true,
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: false create: true
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: false enabled: true
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::new(), sender: KubePrometheus { config },
receivers: self.receivers.clone(), receivers: self.receivers.clone(),
rules: self.rules.clone(), rules: self.rules.clone(),
}) })

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) -> HelmChartScore { pub fn ntfy_helm_chart_score(namespace: String, host: 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: false enabled: true
# 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: ntfy.host.com - host: {host}
paths: paths:
- path: / - path: /
pathType: ImplementationSpecific pathType: ImplementationSpecific

View File

@@ -17,6 +17,7 @@ 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 {
@@ -126,7 +127,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()) ntfy_helm_chart_score(self.score.namespace.clone(), self.score.host.clone())
.create_interpret() .create_interpret()
.execute(inventory, topology) .execute(inventory, topology)
.await?; .await?;

View File

@@ -83,6 +83,7 @@ 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::error; use log::{debug, 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: Nat, pub nat: Option<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: Widgets, pub widgets: Option<Widgets>,
pub revision: Revision, pub revision: Revision,
#[yaserde(rename = "OPNsense")] #[yaserde(rename = "OPNsense")]
pub opnsense: OPNsenseXmlSection, pub opnsense: OPNsenseXmlSection,
@@ -46,10 +46,12 @@ 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")
@@ -242,6 +244,7 @@ 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)]
@@ -271,6 +274,7 @@ 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)]
@@ -1506,7 +1510,7 @@ pub struct Vlans {
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Bridges { pub struct Bridges {
pub bridged: MaybeString, pub bridged: Option<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 = "1.4.1" pretty_assertions.workspace = true