forked from NationTech/harmony
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			master
			...
			idempotent
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9a205b0eb1 | |||
| 9b892dc882 | |||
| a31b459f33 | |||
| 3d8dd4d8e6 | |||
| 01206f5db1 | |||
| fc4c18ccea | |||
| e9a1aa4831 | 
							
								
								
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -429,6 +429,15 @@ dependencies = [ | |||||||
|  "wait-timeout", |  "wait-timeout", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "assertor" | ||||||
|  | version = "0.0.4" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "4ff24d87260733dc86d38a11c60d9400ce4a74a05d0dafa2a6f5ab249cd857cb" | ||||||
|  | dependencies = [ | ||||||
|  |  "num-traits", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "async-broadcast" | name = "async-broadcast" | ||||||
| version = "0.7.2" | version = "0.7.2" | ||||||
| @ -1881,6 +1890,8 @@ dependencies = [ | |||||||
|  "env_logger", |  "env_logger", | ||||||
|  "harmony", |  "harmony", | ||||||
|  "harmony_macros", |  "harmony_macros", | ||||||
|  |  "harmony_secret", | ||||||
|  |  "harmony_secret_derive", | ||||||
|  "harmony_tui", |  "harmony_tui", | ||||||
|  "harmony_types", |  "harmony_types", | ||||||
|  "log", |  "log", | ||||||
| @ -3878,6 +3889,7 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" | |||||||
| name = "opnsense-config" | name = "opnsense-config" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  |  "assertor", | ||||||
|  "async-trait", |  "async-trait", | ||||||
|  "chrono", |  "chrono", | ||||||
|  "env_logger", |  "env_logger", | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Cargo.toml
									
									
									
									
									
								
							| @ -14,7 +14,8 @@ members = [ | |||||||
|   "harmony_composer", |   "harmony_composer", | ||||||
|   "harmony_inventory_agent", |   "harmony_inventory_agent", | ||||||
|   "harmony_secret_derive", |   "harmony_secret_derive", | ||||||
|   "harmony_secret", "adr/agent_discovery/mdns", |   "harmony_secret", | ||||||
|  |   "adr/agent_discovery/mdns", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [workspace.package] | [workspace.package] | ||||||
| @ -66,5 +67,12 @@ thiserror = "2.0.14" | |||||||
| serde = { version = "1.0.209", features = ["derive", "rc"] } | serde = { version = "1.0.209", features = ["derive", "rc"] } | ||||||
| serde_json = "1.0.127" | serde_json = "1.0.127" | ||||||
| askama = "0.14" | askama = "0.14" | ||||||
| sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] } | sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } | ||||||
| reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false } | reqwest = { version = "0.12", features = [ | ||||||
|  |   "blocking", | ||||||
|  |   "stream", | ||||||
|  |   "rustls-tls", | ||||||
|  |   "http2", | ||||||
|  |   "json", | ||||||
|  | ], default-features = false } | ||||||
|  | assertor = "0.0.4" | ||||||
|  | |||||||
| @ -28,13 +28,7 @@ pub trait LoadBalancer: Send + Sync { | |||||||
|         &self, |         &self, | ||||||
|         service: &LoadBalancerService, |         service: &LoadBalancerService, | ||||||
|     ) -> Result<(), ExecutorError> { |     ) -> Result<(), ExecutorError> { | ||||||
|         debug!( |         self.add_service(service).await?; | ||||||
|             "Listing LoadBalancer services {:?}", |  | ||||||
|             self.list_services().await |  | ||||||
|         ); |  | ||||||
|         if !self.list_services().await.contains(service) { |  | ||||||
|             self.add_service(service).await?; |  | ||||||
|         } |  | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -26,19 +26,13 @@ impl LoadBalancer for OPNSenseFirewall { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { |     async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { | ||||||
|         warn!( |  | ||||||
|             "TODO : the current implementation does not check / cleanup / merge with existing haproxy services properly. Make sure to manually verify that the configuration is correct after executing any operation here" |  | ||||||
|         ); |  | ||||||
|         let mut config = self.opnsense_config.write().await; |         let mut config = self.opnsense_config.write().await; | ||||||
|  |         let mut load_balancer = config.load_balancer(); | ||||||
|  | 
 | ||||||
|         let (frontend, backend, servers, healthcheck) = |         let (frontend, backend, servers, healthcheck) = | ||||||
|             harmony_load_balancer_service_to_haproxy_xml(service); |             harmony_load_balancer_service_to_haproxy_xml(service); | ||||||
|         let mut load_balancer = config.load_balancer(); | 
 | ||||||
|         load_balancer.add_backend(backend); |         load_balancer.configure_service(frontend, backend, servers, healthcheck); | ||||||
|         load_balancer.add_frontend(frontend); |  | ||||||
|         load_balancer.add_servers(servers); |  | ||||||
|         if let Some(healthcheck) = healthcheck { |  | ||||||
|             load_balancer.add_healthcheck(healthcheck); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| @ -106,7 +100,7 @@ pub(crate) fn haproxy_xml_config_to_harmony_loadbalancer( | |||||||
|                 .backends |                 .backends | ||||||
|                 .backends |                 .backends | ||||||
|                 .iter() |                 .iter() | ||||||
|                 .find(|b| b.uuid == frontend.default_backend); |                 .find(|b| Some(b.uuid.clone()) == frontend.default_backend); | ||||||
| 
 | 
 | ||||||
|             let mut health_check = None; |             let mut health_check = None; | ||||||
|             match matching_backend { |             match matching_backend { | ||||||
| @ -116,8 +110,7 @@ pub(crate) fn haproxy_xml_config_to_harmony_loadbalancer( | |||||||
|                 } |                 } | ||||||
|                 None => { |                 None => { | ||||||
|                     warn!( |                     warn!( | ||||||
|                         "HAProxy config could not find a matching backend for frontend {:?}", |                         "HAProxy config could not find a matching backend for frontend {frontend:?}" | ||||||
|                         frontend |  | ||||||
|                     ); |                     ); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -152,11 +145,11 @@ pub(crate) fn get_servers_for_backend( | |||||||
|         .servers |         .servers | ||||||
|         .iter() |         .iter() | ||||||
|         .filter_map(|server| { |         .filter_map(|server| { | ||||||
|  |             let address = server.address.clone()?; | ||||||
|  |             let port = server.port?; | ||||||
|  | 
 | ||||||
|             if backend_servers.contains(&server.uuid.as_str()) { |             if backend_servers.contains(&server.uuid.as_str()) { | ||||||
|                 return Some(BackendServer { |                 return Some(BackendServer { address, port }); | ||||||
|                     address: server.address.clone(), |  | ||||||
|                     port: server.port, |  | ||||||
|                 }); |  | ||||||
|             } |             } | ||||||
|             None |             None | ||||||
|         }) |         }) | ||||||
| @ -347,7 +340,7 @@ pub(crate) fn harmony_load_balancer_service_to_haproxy_xml( | |||||||
|         name: format!("frontend_{}", service.listening_port), |         name: format!("frontend_{}", service.listening_port), | ||||||
|         bind: service.listening_port.to_string(), |         bind: service.listening_port.to_string(), | ||||||
|         mode: "tcp".to_string(), // TODO do not depend on health check here
 |         mode: "tcp".to_string(), // TODO do not depend on health check here
 | ||||||
|         default_backend: backend.uuid.clone(), |         default_backend: Some(backend.uuid.clone()), | ||||||
|         ..Default::default() |         ..Default::default() | ||||||
|     }; |     }; | ||||||
|     info!("HAPRoxy frontend and backend mode currently hardcoded to tcp"); |     info!("HAPRoxy frontend and backend mode currently hardcoded to tcp"); | ||||||
| @ -361,8 +354,8 @@ fn server_to_haproxy_server(server: &BackendServer) -> HAProxyServer { | |||||||
|         uuid: Uuid::new_v4().to_string(), |         uuid: Uuid::new_v4().to_string(), | ||||||
|         name: format!("{}_{}", &server.address, &server.port), |         name: format!("{}_{}", &server.address, &server.port), | ||||||
|         enabled: 1, |         enabled: 1, | ||||||
|         address: server.address.clone(), |         address: Some(server.address.clone()), | ||||||
|         port: server.port, |         port: Some(server.port), | ||||||
|         mode: "active".to_string(), |         mode: "active".to_string(), | ||||||
|         server_type: "static".to_string(), |         server_type: "static".to_string(), | ||||||
|         ..Default::default() |         ..Default::default() | ||||||
| @ -385,8 +378,8 @@ mod tests { | |||||||
|         let mut haproxy = HAProxy::default(); |         let mut haproxy = HAProxy::default(); | ||||||
|         let server = HAProxyServer { |         let server = HAProxyServer { | ||||||
|             uuid: "server1".to_string(), |             uuid: "server1".to_string(), | ||||||
|             address: "192.168.1.1".to_string(), |             address: Some("192.168.1.1".to_string()), | ||||||
|             port: 80, |             port: Some(80), | ||||||
|             ..Default::default() |             ..Default::default() | ||||||
|         }; |         }; | ||||||
|         haproxy.servers.servers.push(server); |         haproxy.servers.servers.push(server); | ||||||
| @ -411,8 +404,8 @@ mod tests { | |||||||
|         let mut haproxy = HAProxy::default(); |         let mut haproxy = HAProxy::default(); | ||||||
|         let server = HAProxyServer { |         let server = HAProxyServer { | ||||||
|             uuid: "server1".to_string(), |             uuid: "server1".to_string(), | ||||||
|             address: "192.168.1.1".to_string(), |             address: Some("192.168.1.1".to_string()), | ||||||
|             port: 80, |             port: Some(80), | ||||||
|             ..Default::default() |             ..Default::default() | ||||||
|         }; |         }; | ||||||
|         haproxy.servers.servers.push(server); |         haproxy.servers.servers.push(server); | ||||||
| @ -431,8 +424,8 @@ mod tests { | |||||||
|         let mut haproxy = HAProxy::default(); |         let mut haproxy = HAProxy::default(); | ||||||
|         let server = HAProxyServer { |         let server = HAProxyServer { | ||||||
|             uuid: "server1".to_string(), |             uuid: "server1".to_string(), | ||||||
|             address: "192.168.1.1".to_string(), |             address: Some("192.168.1.1".to_string()), | ||||||
|             port: 80, |             port: Some(80), | ||||||
|             ..Default::default() |             ..Default::default() | ||||||
|         }; |         }; | ||||||
|         haproxy.servers.servers.push(server); |         haproxy.servers.servers.push(server); | ||||||
| @ -453,16 +446,16 @@ mod tests { | |||||||
|         let mut haproxy = HAProxy::default(); |         let mut haproxy = HAProxy::default(); | ||||||
|         let server = HAProxyServer { |         let server = HAProxyServer { | ||||||
|             uuid: "server1".to_string(), |             uuid: "server1".to_string(), | ||||||
|             address: "some-hostname.test.mcd".to_string(), |             address: Some("some-hostname.test.mcd".to_string()), | ||||||
|             port: 80, |             port: Some(80), | ||||||
|             ..Default::default() |             ..Default::default() | ||||||
|         }; |         }; | ||||||
|         haproxy.servers.servers.push(server); |         haproxy.servers.servers.push(server); | ||||||
| 
 | 
 | ||||||
|         let server = HAProxyServer { |         let server = HAProxyServer { | ||||||
|             uuid: "server2".to_string(), |             uuid: "server2".to_string(), | ||||||
|             address: "192.168.1.2".to_string(), |             address: Some("192.168.1.2".to_string()), | ||||||
|             port: 8080, |             port: Some(8080), | ||||||
|             ..Default::default() |             ..Default::default() | ||||||
|         }; |         }; | ||||||
|         haproxy.servers.servers.push(server); |         haproxy.servers.servers.push(server); | ||||||
|  | |||||||
| @ -77,6 +77,8 @@ impl OKDBootstrapLoadBalancerScore { | |||||||
|             address: topology.bootstrap_host.ip.to_string(), |             address: topology.bootstrap_host.ip.to_string(), | ||||||
|             port, |             port, | ||||||
|         }); |         }); | ||||||
|  | 
 | ||||||
|  |         backend.dedup(); | ||||||
|         backend |         backend | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -77,7 +77,7 @@ impl YaSerializeTrait for HAProxyId { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(PartialEq, Debug)] | #[derive(PartialEq, Debug, Clone)] | ||||||
| pub struct HAProxyId(String); | pub struct HAProxyId(String); | ||||||
| 
 | 
 | ||||||
| impl Default for HAProxyId { | impl Default for HAProxyId { | ||||||
| @ -297,7 +297,7 @@ pub struct HAProxyFrontends { | |||||||
|     pub frontend: Vec<Frontend>, |     pub frontend: Vec<Frontend>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] | #[derive(Clone, Default, PartialEq, Debug, YaSerialize, YaDeserialize)] | ||||||
| pub struct Frontend { | pub struct Frontend { | ||||||
|     #[yaserde(attribute = true)] |     #[yaserde(attribute = true)] | ||||||
|     pub uuid: String, |     pub uuid: String, | ||||||
| @ -310,7 +310,7 @@ pub struct Frontend { | |||||||
|     pub bind_options: MaybeString, |     pub bind_options: MaybeString, | ||||||
|     pub mode: String, |     pub mode: String, | ||||||
|     #[yaserde(rename = "defaultBackend")] |     #[yaserde(rename = "defaultBackend")] | ||||||
|     pub default_backend: String, |     pub default_backend: Option<String>, | ||||||
|     pub ssl_enabled: i32, |     pub ssl_enabled: i32, | ||||||
|     pub ssl_certificates: MaybeString, |     pub ssl_certificates: MaybeString, | ||||||
|     pub ssl_default_certificate: MaybeString, |     pub ssl_default_certificate: MaybeString, | ||||||
| @ -416,7 +416,7 @@ pub struct HAProxyBackends { | |||||||
|     pub backends: Vec<HAProxyBackend>, |     pub backends: Vec<HAProxyBackend>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] | #[derive(Clone, Default, PartialEq, Debug, YaSerialize, YaDeserialize)] | ||||||
| pub struct HAProxyBackend { | pub struct HAProxyBackend { | ||||||
|     #[yaserde(attribute = true, rename = "uuid")] |     #[yaserde(attribute = true, rename = "uuid")] | ||||||
|     pub uuid: String, |     pub uuid: String, | ||||||
| @ -535,7 +535,7 @@ pub struct HAProxyServers { | |||||||
|     pub servers: Vec<HAProxyServer>, |     pub servers: Vec<HAProxyServer>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] | #[derive(Clone, Default, PartialEq, Debug, YaSerialize, YaDeserialize)] | ||||||
| pub struct HAProxyServer { | pub struct HAProxyServer { | ||||||
|     #[yaserde(attribute = true, rename = "uuid")] |     #[yaserde(attribute = true, rename = "uuid")] | ||||||
|     pub uuid: String, |     pub uuid: String, | ||||||
| @ -543,8 +543,8 @@ pub struct HAProxyServer { | |||||||
|     pub enabled: u8, |     pub enabled: u8, | ||||||
|     pub name: String, |     pub name: String, | ||||||
|     pub description: MaybeString, |     pub description: MaybeString, | ||||||
|     pub address: String, |     pub address: Option<String>, | ||||||
|     pub port: u16, |     pub port: Option<u16>, | ||||||
|     pub checkport: MaybeString, |     pub checkport: MaybeString, | ||||||
|     pub mode: String, |     pub mode: String, | ||||||
|     pub multiplexer_protocol: MaybeString, |     pub multiplexer_protocol: MaybeString, | ||||||
| @ -589,7 +589,7 @@ pub struct HAProxyHealthChecks { | |||||||
|     pub healthchecks: Vec<HAProxyHealthCheck>, |     pub healthchecks: Vec<HAProxyHealthCheck>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] | #[derive(Clone, Default, PartialEq, Debug, YaSerialize, YaDeserialize)] | ||||||
| pub struct HAProxyHealthCheck { | pub struct HAProxyHealthCheck { | ||||||
|     #[yaserde(attribute = true)] |     #[yaserde(attribute = true)] | ||||||
|     pub uuid: String, |     pub uuid: String, | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ sha2 = "0.10.9" | |||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| pretty_assertions.workspace = true | pretty_assertions.workspace = true | ||||||
|  | assertor.workspace = true | ||||||
| 
 | 
 | ||||||
| [lints.rust] | [lints.rust] | ||||||
| unexpected_cfgs = { level = "warn", check-cfg = ['cfg(e2e_test)'] } | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(e2e_test)'] } | ||||||
|  | |||||||
| @ -30,8 +30,7 @@ impl SshConfigManager { | |||||||
| 
 | 
 | ||||||
|         self.opnsense_shell |         self.opnsense_shell | ||||||
|             .exec(&format!( |             .exec(&format!( | ||||||
|                 "cp /conf/config.xml /conf/backup/{}", |                 "cp /conf/config.xml /conf/backup/{backup_filename}" | ||||||
|                 backup_filename |  | ||||||
|             )) |             )) | ||||||
|             .await |             .await | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,9 +1,7 @@ | |||||||
| mod ssh; | mod ssh; | ||||||
| pub use ssh::*; |  | ||||||
| 
 |  | ||||||
| use async_trait::async_trait; |  | ||||||
| 
 |  | ||||||
| use crate::Error; | use crate::Error; | ||||||
|  | use async_trait::async_trait; | ||||||
|  | pub use ssh::*; | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| pub trait OPNsenseShell: std::fmt::Debug + Send + Sync { | pub trait OPNsenseShell: std::fmt::Debug + Send + Sync { | ||||||
|  | |||||||
| @ -1,11 +1,8 @@ | |||||||
| use std::sync::Arc; | use crate::{config::OPNsenseShell, Error}; | ||||||
| 
 |  | ||||||
| use log::warn; |  | ||||||
| use opnsense_config_xml::{ | use opnsense_config_xml::{ | ||||||
|     Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer, OPNsense, |     Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer, OPNsense, | ||||||
| }; | }; | ||||||
| 
 | use std::{collections::HashSet, sync::Arc}; | ||||||
| use crate::{config::OPNsenseShell, Error}; |  | ||||||
| 
 | 
 | ||||||
| pub struct LoadBalancerConfig<'a> { | pub struct LoadBalancerConfig<'a> { | ||||||
|     opnsense: &'a mut OPNsense, |     opnsense: &'a mut OPNsense, | ||||||
| @ -31,7 +28,7 @@ impl<'a> LoadBalancerConfig<'a> { | |||||||
|         match &mut self.opnsense.opnsense.haproxy.as_mut() { |         match &mut self.opnsense.opnsense.haproxy.as_mut() { | ||||||
|             Some(haproxy) => f(haproxy), |             Some(haproxy) => f(haproxy), | ||||||
|             None => unimplemented!( |             None => unimplemented!( | ||||||
|                 "Adding a backend is not supported when haproxy config does not exist yet" |                 "Cannot configure load balancer when haproxy config does not exist yet" | ||||||
|             ), |             ), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -40,21 +37,67 @@ impl<'a> LoadBalancerConfig<'a> { | |||||||
|         self.with_haproxy(|haproxy| haproxy.general.enabled = enabled as i32); |         self.with_haproxy(|haproxy| haproxy.general.enabled = enabled as i32); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn add_backend(&mut self, backend: HAProxyBackend) { |     /// Configures a service by removing any existing service on the same port
 | ||||||
|         warn!("TODO make sure this new backend does not refer non-existing entities like servers or health checks"); |     /// and then adding the new definition. This ensures idempotency.
 | ||||||
|         self.with_haproxy(|haproxy| haproxy.backends.backends.push(backend)); |     pub fn configure_service( | ||||||
|  |         &mut self, | ||||||
|  |         frontend: Frontend, | ||||||
|  |         backend: HAProxyBackend, | ||||||
|  |         servers: Vec<HAProxyServer>, | ||||||
|  |         healthcheck: Option<HAProxyHealthCheck>, | ||||||
|  |     ) { | ||||||
|  |         self.remove_service_by_bind_address(&frontend.bind); | ||||||
|  |         self.remove_servers(&servers); | ||||||
|  | 
 | ||||||
|  |         self.add_new_service(frontend, backend, servers, healthcheck); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn add_frontend(&mut self, frontend: Frontend) { |     // Remove the corresponding real servers based on their name if they already exist.
 | ||||||
|         self.with_haproxy(|haproxy| haproxy.frontends.frontend.push(frontend)); |     fn remove_servers(&mut self, servers: &[HAProxyServer]) { | ||||||
|  |         let server_names: HashSet<_> = servers.iter().map(|s| s.name.clone()).collect(); | ||||||
|  |         self.with_haproxy(|haproxy| { | ||||||
|  |             haproxy | ||||||
|  |                 .servers | ||||||
|  |                 .servers | ||||||
|  |                 .retain(|s| !server_names.contains(&s.name)); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn add_healthcheck(&mut self, healthcheck: HAProxyHealthCheck) { |     /// Removes a service and its dependent components based on the frontend's bind address.
 | ||||||
|         self.with_haproxy(|haproxy| haproxy.healthchecks.healthchecks.push(healthcheck)); |     /// This performs a cascading delete of the frontend, backend, servers, and health check.
 | ||||||
|  |     fn remove_service_by_bind_address(&mut self, bind_address: &str) { | ||||||
|  |         self.with_haproxy(|haproxy| { | ||||||
|  |             let Some(old_frontend) = remove_frontend_by_bind_address(haproxy, bind_address) else { | ||||||
|  |                 return; | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             let Some(old_backend) = remove_backend(haproxy, old_frontend) else { | ||||||
|  |                 return; | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             remove_healthcheck(haproxy, &old_backend); | ||||||
|  |             remove_linked_servers(haproxy, &old_backend); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn add_servers(&mut self, mut servers: Vec<HAProxyServer>) { |     /// Adds the components of a new service to the HAProxy configuration.
 | ||||||
|         self.with_haproxy(|haproxy| haproxy.servers.servers.append(&mut servers)); |     /// This function de-duplicates servers by name to prevent configuration errors.
 | ||||||
|  |     fn add_new_service( | ||||||
|  |         &mut self, | ||||||
|  |         frontend: Frontend, | ||||||
|  |         backend: HAProxyBackend, | ||||||
|  |         servers: Vec<HAProxyServer>, | ||||||
|  |         healthcheck: Option<HAProxyHealthCheck>, | ||||||
|  |     ) { | ||||||
|  |         self.with_haproxy(|haproxy| { | ||||||
|  |             if let Some(check) = healthcheck { | ||||||
|  |                 haproxy.healthchecks.healthchecks.push(check); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             haproxy.servers.servers.extend(servers); | ||||||
|  |             haproxy.backends.backends.push(backend); | ||||||
|  |             haproxy.frontends.frontend.push(frontend); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn reload_restart(&self) -> Result<(), Error> { |     pub async fn reload_restart(&self) -> Result<(), Error> { | ||||||
| @ -82,3 +125,262 @@ impl<'a> LoadBalancerConfig<'a> { | |||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | fn remove_frontend_by_bind_address(haproxy: &mut HAProxy, bind_address: &str) -> Option<Frontend> { | ||||||
|  |     let pos = haproxy | ||||||
|  |         .frontends | ||||||
|  |         .frontend | ||||||
|  |         .iter() | ||||||
|  |         .position(|f| f.bind == bind_address); | ||||||
|  | 
 | ||||||
|  |     match pos { | ||||||
|  |         Some(pos) => Some(haproxy.frontends.frontend.remove(pos)), | ||||||
|  |         None => None, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn remove_backend(haproxy: &mut HAProxy, old_frontend: Frontend) -> Option<HAProxyBackend> { | ||||||
|  |     let default_backend = old_frontend.default_backend?; | ||||||
|  |     let pos = haproxy | ||||||
|  |         .backends | ||||||
|  |         .backends | ||||||
|  |         .iter() | ||||||
|  |         .position(|b| b.uuid == default_backend); | ||||||
|  | 
 | ||||||
|  |     match pos { | ||||||
|  |         Some(pos) => Some(haproxy.backends.backends.remove(pos)), | ||||||
|  |         None => None, // orphaned frontend, shouldn't happen
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn remove_healthcheck(haproxy: &mut HAProxy, backend: &HAProxyBackend) { | ||||||
|  |     if let Some(uuid) = &backend.health_check.content { | ||||||
|  |         haproxy | ||||||
|  |             .healthchecks | ||||||
|  |             .healthchecks | ||||||
|  |             .retain(|h| h.uuid != *uuid); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Remove the backend's servers. This assumes servers are not shared between services.
 | ||||||
|  | fn remove_linked_servers(haproxy: &mut HAProxy, backend: &HAProxyBackend) { | ||||||
|  |     if let Some(server_uuids_str) = &backend.linked_servers.content { | ||||||
|  |         let server_uuids_to_remove: HashSet<_> = server_uuids_str.split(',').collect(); | ||||||
|  |         haproxy | ||||||
|  |             .servers | ||||||
|  |             .servers | ||||||
|  |             .retain(|s| !server_uuids_to_remove.contains(s.uuid.as_str())); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use crate::config::DummyOPNSenseShell; | ||||||
|  |     use assertor::*; | ||||||
|  |     use opnsense_config_xml::{ | ||||||
|  |         Frontend, HAProxy, HAProxyBackend, HAProxyBackends, HAProxyFrontends, HAProxyHealthCheck, | ||||||
|  |         HAProxyHealthChecks, HAProxyId, HAProxyServer, HAProxyServers, MaybeString, OPNsense, | ||||||
|  |     }; | ||||||
|  |     use std::sync::Arc; | ||||||
|  | 
 | ||||||
|  |     use super::LoadBalancerConfig; | ||||||
|  | 
 | ||||||
|  |     static SERVICE_BIND_ADDRESS: &str = "192.168.1.1:80"; | ||||||
|  |     static OTHER_SERVICE_BIND_ADDRESS: &str = "192.168.1.1:443"; | ||||||
|  | 
 | ||||||
|  |     static SERVER_ADDRESS: &str = "1.1.1.1:80"; | ||||||
|  |     static OTHER_SERVER_ADDRESS: &str = "1.1.1.1:443"; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn configure_service_should_add_all_service_components_to_haproxy() { | ||||||
|  |         let mut opnsense = given_opnsense(); | ||||||
|  |         let mut load_balancer = given_load_balancer(&mut opnsense); | ||||||
|  |         let (healthcheck, servers, backend, frontend) = | ||||||
|  |             given_service(SERVICE_BIND_ADDRESS, SERVER_ADDRESS); | ||||||
|  | 
 | ||||||
|  |         load_balancer.configure_service( | ||||||
|  |             frontend.clone(), | ||||||
|  |             backend.clone(), | ||||||
|  |             servers.clone(), | ||||||
|  |             Some(healthcheck.clone()), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_haproxy_configured_with( | ||||||
|  |             opnsense, | ||||||
|  |             vec![frontend], | ||||||
|  |             vec![backend], | ||||||
|  |             servers, | ||||||
|  |             vec![healthcheck], | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn configure_service_should_replace_service_on_same_bind_address() { | ||||||
|  |         let (healthcheck, servers, backend, frontend) = | ||||||
|  |             given_service(SERVICE_BIND_ADDRESS, SERVER_ADDRESS); | ||||||
|  |         let mut opnsense = given_opnsense_with(given_haproxy( | ||||||
|  |             vec![frontend.clone()], | ||||||
|  |             vec![backend.clone()], | ||||||
|  |             servers.clone(), | ||||||
|  |             vec![healthcheck.clone()], | ||||||
|  |         )); | ||||||
|  |         let mut load_balancer = given_load_balancer(&mut opnsense); | ||||||
|  | 
 | ||||||
|  |         let (updated_healthcheck, updated_servers, updated_backend, updated_frontend) = | ||||||
|  |             given_service(SERVICE_BIND_ADDRESS, OTHER_SERVER_ADDRESS); | ||||||
|  | 
 | ||||||
|  |         load_balancer.configure_service( | ||||||
|  |             updated_frontend.clone(), | ||||||
|  |             updated_backend.clone(), | ||||||
|  |             updated_servers.clone(), | ||||||
|  |             Some(updated_healthcheck.clone()), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_haproxy_configured_with( | ||||||
|  |             opnsense, | ||||||
|  |             vec![updated_frontend], | ||||||
|  |             vec![updated_backend], | ||||||
|  |             updated_servers, | ||||||
|  |             vec![updated_healthcheck], | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn configure_service_should_keep_existing_service_on_different_bind_addresses() { | ||||||
|  |         let (healthcheck, servers, backend, frontend) = | ||||||
|  |             given_service(SERVICE_BIND_ADDRESS, SERVER_ADDRESS); | ||||||
|  |         let (other_healthcheck, other_servers, other_backend, other_frontend) = | ||||||
|  |             given_service(OTHER_SERVICE_BIND_ADDRESS, OTHER_SERVER_ADDRESS); | ||||||
|  |         let mut opnsense = given_opnsense_with(given_haproxy( | ||||||
|  |             vec![frontend.clone()], | ||||||
|  |             vec![backend.clone()], | ||||||
|  |             servers.clone(), | ||||||
|  |             vec![healthcheck.clone()], | ||||||
|  |         )); | ||||||
|  |         let mut load_balancer = given_load_balancer(&mut opnsense); | ||||||
|  | 
 | ||||||
|  |         load_balancer.configure_service( | ||||||
|  |             other_frontend.clone(), | ||||||
|  |             other_backend.clone(), | ||||||
|  |             other_servers.clone(), | ||||||
|  |             Some(other_healthcheck.clone()), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert_haproxy_configured_with( | ||||||
|  |             opnsense, | ||||||
|  |             vec![frontend, other_frontend], | ||||||
|  |             vec![backend, other_backend], | ||||||
|  |             [servers, other_servers].concat(), | ||||||
|  |             vec![healthcheck, other_healthcheck], | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn assert_haproxy_configured_with( | ||||||
|  |         opnsense: OPNsense, | ||||||
|  |         frontends: Vec<Frontend>, | ||||||
|  |         backends: Vec<HAProxyBackend>, | ||||||
|  |         servers: Vec<HAProxyServer>, | ||||||
|  |         healthchecks: Vec<HAProxyHealthCheck>, | ||||||
|  |     ) { | ||||||
|  |         let haproxy = opnsense.opnsense.haproxy.as_ref().unwrap(); | ||||||
|  |         assert_that!(haproxy.frontends.frontend).contains_exactly(frontends); | ||||||
|  |         assert_that!(haproxy.backends.backends).contains_exactly(backends); | ||||||
|  |         assert_that!(haproxy.servers.servers).is_equal_to(servers); | ||||||
|  |         assert_that!(haproxy.healthchecks.healthchecks).contains_exactly(healthchecks); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_opnsense() -> OPNsense { | ||||||
|  |         OPNsense::default() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_opnsense_with(haproxy: HAProxy) -> OPNsense { | ||||||
|  |         let mut opnsense = OPNsense::default(); | ||||||
|  |         opnsense.opnsense.haproxy = Some(haproxy); | ||||||
|  | 
 | ||||||
|  |         opnsense | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_load_balancer<'a>(opnsense: &'a mut OPNsense) -> LoadBalancerConfig<'a> { | ||||||
|  |         let opnsense_shell = Arc::new(DummyOPNSenseShell {}); | ||||||
|  |         if opnsense.opnsense.haproxy.is_none() { | ||||||
|  |             opnsense.opnsense.haproxy = Some(HAProxy::default()); | ||||||
|  |         } | ||||||
|  |         LoadBalancerConfig::new(opnsense, opnsense_shell) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_service( | ||||||
|  |         bind_address: &str, | ||||||
|  |         server_address: &str, | ||||||
|  |     ) -> ( | ||||||
|  |         HAProxyHealthCheck, | ||||||
|  |         Vec<HAProxyServer>, | ||||||
|  |         HAProxyBackend, | ||||||
|  |         Frontend, | ||||||
|  |     ) { | ||||||
|  |         let healthcheck = given_healthcheck(); | ||||||
|  |         let servers = vec![given_server(server_address)]; | ||||||
|  |         let backend = given_backend(); | ||||||
|  |         let frontend = given_frontend(bind_address); | ||||||
|  |         (healthcheck, servers, backend, frontend) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_haproxy( | ||||||
|  |         frontends: Vec<Frontend>, | ||||||
|  |         backends: Vec<HAProxyBackend>, | ||||||
|  |         servers: Vec<HAProxyServer>, | ||||||
|  |         healthchecks: Vec<HAProxyHealthCheck>, | ||||||
|  |     ) -> HAProxy { | ||||||
|  |         HAProxy { | ||||||
|  |             frontends: HAProxyFrontends { | ||||||
|  |                 frontend: frontends, | ||||||
|  |             }, | ||||||
|  |             backends: HAProxyBackends { backends }, | ||||||
|  |             servers: HAProxyServers { servers }, | ||||||
|  |             healthchecks: HAProxyHealthChecks { healthchecks }, | ||||||
|  |             ..Default::default() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_frontend(bind_address: &str) -> Frontend { | ||||||
|  |         Frontend { | ||||||
|  |             uuid: "uuid".into(), | ||||||
|  |             id: HAProxyId::default(), | ||||||
|  |             enabled: 1, | ||||||
|  |             name: format!("frontend_{bind_address}"), | ||||||
|  |             bind: bind_address.into(), | ||||||
|  |             default_backend: Some("backend-uuid".into()), | ||||||
|  |             ..Default::default() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_backend() -> HAProxyBackend { | ||||||
|  |         HAProxyBackend { | ||||||
|  |             uuid: "backend-uuid".into(), | ||||||
|  |             id: HAProxyId::default(), | ||||||
|  |             enabled: 1, | ||||||
|  |             name: "backend_192.168.1.1:80".into(), | ||||||
|  |             linked_servers: MaybeString::from("server-uuid"), | ||||||
|  |             health_check_enabled: 1, | ||||||
|  |             health_check: MaybeString::from("healthcheck-uuid"), | ||||||
|  |             ..Default::default() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_server(address: &str) -> HAProxyServer { | ||||||
|  |         HAProxyServer { | ||||||
|  |             uuid: "server-uuid".into(), | ||||||
|  |             id: HAProxyId::default(), | ||||||
|  |             name: address.into(), | ||||||
|  |             address: Some(address.into()), | ||||||
|  |             ..Default::default() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn given_healthcheck() -> HAProxyHealthCheck { | ||||||
|  |         HAProxyHealthCheck { | ||||||
|  |             uuid: "healthcheck-uuid".into(), | ||||||
|  |             name: "healthcheck".into(), | ||||||
|  |             ..Default::default() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user