From 670b701f6aefa540fe51908f0db6221590bf48a2 Mon Sep 17 00:00:00 2001 From: tahahawa Date: Wed, 16 Jul 2025 02:12:58 -0400 Subject: [PATCH] use figment and try to make an "upgradeable firewall" --- Cargo.lock | 80 +++++++++++++++++++ Cargo.toml | 1 + examples/opnsense/src/main.rs | 6 +- harmony/Cargo.toml | 1 + harmony/src/domain/config.rs | 73 ++++++++++++++--- harmony/src/domain/topology/ha_cluster.rs | 22 ++++- harmony/src/domain/topology/k8s.rs | 6 +- harmony/src/domain/topology/k8s_anywhere.rs | 41 +++++----- harmony/src/domain/topology/network.rs | 17 +++- .../features/continuous_delivery.rs | 6 +- harmony/src/modules/application/rust.rs | 12 ++- harmony/src/modules/k3d/install.rs | 6 +- harmony/src/modules/lamp.rs | 9 ++- opnsense-config-xml/src/data/opnsense.rs | 3 +- 14 files changed, 236 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1eb805d..3f996c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,6 +219,15 @@ 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" @@ -409,6 +418,12 @@ 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" @@ -1429,6 +1444,19 @@ 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" @@ -1751,6 +1779,7 @@ dependencies = [ "dyn-clone", "email_address", "env_logger", + "figment", "fqdn", "futures-util", "harmony_macros", @@ -2416,6 +2445,12 @@ 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" @@ -3237,6 +3272,29 @@ 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" @@ -3499,6 +3557,19 @@ 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" @@ -5141,6 +5212,15 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index 22645f3..b245427 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,3 +56,4 @@ pretty_assertions = "1.4.1" bollard = "0.19.1" base64 = "0.22.1" tar = "0.4.44" +figment = { version = "0.10.19", features = ["env"] } diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index 7f1d190..685ff6e 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -97,9 +97,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(OPNSenseLaunchUpgrade { + // opnsense: opnsense.get_opnsense_config(), + // }), Box::new(SuccessScore {}), Box::new(ErrorScore {}), Box::new(PanicScore {}), diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 87b97ac..f3aeb4d 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -62,6 +62,7 @@ serde_with = "3.14.0" bollard.workspace = true tar.workspace = true base64.workspace = true +figment.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony/src/domain/config.rs b/harmony/src/domain/config.rs index 20f08a2..bec3eee 100644 --- a/harmony/src/domain/config.rs +++ b/harmony/src/domain/config.rs @@ -1,15 +1,66 @@ +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; -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)); +#[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 { + Figment::from(Config::default()) + .merge(Env::prefixed("HARMONY_")) + .extract() + } + + fn from(provider: T) -> Result { + 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, Error> { + figment::providers::Serialized::defaults(Config::default()).data() + } + + fn profile(&self) -> Option { + // Optionally, a profile that's selected by default. + Some(Profile::Default) + } } diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 010f922..6818e2d 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -4,6 +4,7 @@ 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; @@ -28,9 +29,12 @@ 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(Debug, Clone)] +#[derive(Clone, Debug)] pub struct HAClusterTopology { pub domain_name: String, pub router: Arc, @@ -55,6 +59,11 @@ impl Topology for HAClusterTopology { 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." ); + 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())) } } @@ -255,6 +264,13 @@ impl Topology for DummyInfra { } } +#[async_trait] +impl Upgradeable 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 { @@ -425,6 +441,8 @@ impl DnsServer for DummyInfra { #[async_trait] impl Upgradeable for HAClusterTopology { async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> { - todo!("implement upgrades for all parts of the cluster") + error!("TODO implement upgrades for all parts of the cluster"); + self.firewall.upgrade(inventory, topology).await?; + Ok(()) } } diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index 6ab249a..d50b965 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -20,6 +20,8 @@ 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, @@ -154,7 +156,9 @@ impl K8sClient { .as_ref() .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 { Ok(current) => { trace!("Received current value {current:#?}"); diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 81e4546..7d63f40 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,9 +1,10 @@ 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::Serialize; +use serde::{Deserialize, Serialize}; use tokio::sync::OnceCell; use crate::{ @@ -219,7 +220,7 @@ impl K8sAnywhereTopology { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] pub struct K8sAnywhereConfig { /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes /// cluster @@ -246,25 +247,29 @@ pub struct K8sAnywhereConfig { /// /// default: true 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 { fn from_env() -> Self { - 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)), - } + Figment::new() + .merge(Env::prefixed("HARMONY_")) + .merge(Env::raw().only(&["KUBECONFIG"])) + .extract() + .expect("couldn't load config from env") } } @@ -304,7 +309,7 @@ impl MultiTargetTopology for K8sAnywhereTopology { return DeploymentTarget::LocalDev; } - match self.config.harmony_profile.to_lowercase().as_str() { + match self.config.profile.to_lowercase().as_str() { "staging" => DeploymentTarget::Staging, "production" => DeploymentTarget::Production, _ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"), diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 42ff8c3..4fb3850 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -2,9 +2,15 @@ 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; +use crate::{ + executors::ExecutorError, + interpret::InterpretError, + inventory::Inventory, + topology::{Topology, upgradeable::Upgradeable}, +}; use super::{IpAddress, LogicalHost, k8s::K8sClient}; @@ -38,6 +44,15 @@ impl std::fmt::Debug for dyn Firewall { } } +// #[async_trait] +// impl Upgradeable 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, } diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs index 39513ab..ee885c3 100644 --- a/harmony/src/modules/application/features/continuous_delivery.rs +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -6,7 +6,7 @@ use serde_yaml::Value; use tempfile::NamedTempFile; use crate::{ - config::HARMONY_DATA_DIR, + config::Config, data::Version, inventory::Inventory, modules::application::{ @@ -56,12 +56,14 @@ impl ContinuousDelivery { 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 = (*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 --- info!( "Importing image '{}' into k3d cluster 'harmony'", diff --git a/harmony/src/modules/application/rust.rs b/harmony/src/modules/application/rust.rs index 62d732b..5188fef 100644 --- a/harmony/src/modules/application/rust.rs +++ b/harmony/src/modules/application/rust.rs @@ -14,7 +14,7 @@ use log::{debug, error, info}; use serde::Serialize; use tar::Archive; -use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; +use crate::config::Config; use crate::{ score::Score, topology::{Topology, Url}, @@ -134,10 +134,12 @@ impl OCICompliant for RustWebapp { } fn image_name(&self) -> String { + let config = Config::load().expect("couldn't load config"); + format!( "{}/{}/{}", - *REGISTRY_URL, - *REGISTRY_PROJECT, + config.registry_url, + config.registry_project, &self.local_image_name() ) } @@ -575,9 +577,11 @@ spec: &self, packaged_chart_path: &PathBuf, ) -> Result> { + 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://{}/{}", *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); info!( diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 18b91a0..813d7f6 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -5,7 +5,7 @@ use log::info; use serde::Serialize; use crate::{ - config::HARMONY_DATA_DIR, + config::Config, data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, @@ -21,8 +21,10 @@ pub struct K3DInstallationScore { impl Default for K3DInstallationScore { fn default() -> Self { + let config = Config::load().expect("couldn't load config"); + Self { - installation_path: HARMONY_DATA_DIR.join("k3d"), + installation_path: config.data_dir.join("k3d"), cluster_name: "harmony".to_string(), } } diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 3c5c439..71f792d 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use log::{debug, info}; use serde::Serialize; -use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; +use crate::config::Config as HarmonyConfig; use crate::modules::k8s::ingress::K8sIngressScore; use crate::topology::HelmCommand; use crate::{ @@ -355,7 +355,12 @@ opcache.fast_shutdown=1 } fn push_docker_image(&self, image_name: &str) -> Result> { - 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") .args(["tag", image_name, &full_tag]) .output()?; diff --git a/opnsense-config-xml/src/data/opnsense.rs b/opnsense-config-xml/src/data/opnsense.rs index 3da70f5..4f4a0cd 100644 --- a/opnsense-config-xml/src/data/opnsense.rs +++ b/opnsense-config-xml/src/data/opnsense.rs @@ -274,6 +274,7 @@ pub struct Group { pub member: Vec, #[yaserde(rename = "priv")] pub priv_field: String, + pub source_networks: Vec, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1509,7 +1510,7 @@ pub struct Vlans { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Bridges { - pub bridged: MaybeString, + pub bridged: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]