use figment and try to make an "upgradeable firewall"

This commit is contained in:
tahahawa 2025-07-16 02:12:58 -04:00
parent 1eaae2016a
commit 670b701f6a
14 changed files with 236 additions and 47 deletions

80
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"
@ -1429,6 +1444,19 @@ 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]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.25" version = "0.2.25"
@ -1751,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",
@ -2416,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"
@ -3237,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"
@ -3499,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"
@ -5141,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"

View File

@ -56,3 +56,4 @@ pretty_assertions = "1.4.1"
bollard = "0.19.1" bollard = "0.19.1"
base64 = "0.22.1" base64 = "0.22.1"
tar = "0.4.44" tar = "0.4.44"
figment = { version = "0.10.19", features = ["env"] }

View File

@ -97,9 +97,9 @@ async fn main() {
opnsense: opnsense.get_opnsense_config(), opnsense: opnsense.get_opnsense_config(),
command: "touch /tmp/helloharmonytouching".to_string(), command: "touch /tmp/helloharmonytouching".to_string(),
}), }),
Box::new(OPNSenseLaunchUpgrade { // Box::new(OPNSenseLaunchUpgrade {
opnsense: opnsense.get_opnsense_config(), // opnsense: opnsense.get_opnsense_config(),
}), // }),
Box::new(SuccessScore {}), Box::new(SuccessScore {}),
Box::new(ErrorScore {}), Box::new(ErrorScore {}),
Box::new(PanicScore {}), Box::new(PanicScore {}),

View File

@ -62,6 +62,7 @@ serde_with = "3.14.0"
bollard.workspace = true bollard.workspace = true
tar.workspace = true tar.workspace = true
base64.workspace = true base64.workspace = true
figment.workspace = true
[dev-dependencies] [dev-dependencies]
pretty_assertions.workspace = true 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

@ -4,6 +4,7 @@ use harmony_types::net::MacAddress;
use log::error; 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;
@ -28,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>,
@ -55,6 +59,11 @@ impl Topology for HAClusterTopology {
error!( 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())) Ok(Outcome::success("for now do nothing".to_string()))
} }
} }
@ -255,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 {
@ -425,6 +441,8 @@ impl DnsServer for DummyInfra {
#[async_trait] #[async_trait]
impl<T: Topology> Upgradeable<T> for HAClusterTopology { impl<T: Topology> Upgradeable<T> for HAClusterTopology {
async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> { 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(())
} }
} }

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:#?}");

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

@ -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

@ -6,7 +6,7 @@ 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::application::{ modules::application::{
@ -56,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'",

View File

@ -14,7 +14,7 @@ use log::{debug, error, info};
use serde::Serialize; use serde::Serialize;
use tar::Archive; 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},
@ -134,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()
) )
} }
@ -575,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

@ -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

@ -274,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)]
@ -1509,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)]