feat: add support for custom CIDR ingress/egress rules #60
							
								
								
									
										4
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -394,6 +394,9 @@ name = "cidr" | ||||
| version = "0.2.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "cipher" | ||||
| @ -1476,6 +1479,7 @@ dependencies = [ | ||||
| name = "harmony_macros" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "cidr", | ||||
|  "harmony_types", | ||||
|  "quote", | ||||
|  "serde", | ||||
|  | ||||
| @ -24,7 +24,7 @@ env_logger = "0.11.5" | ||||
| derive-new = "0.7.0" | ||||
| async-trait = "0.1.82" | ||||
| tokio = { version = "1.40.0", features = ["io-std", "fs", "macros", "rt-multi-thread"] } | ||||
| cidr = "0.2.3" | ||||
| cidr = { features = ["serde"], version = "0.2" } | ||||
| russh = "0.45.0" | ||||
| russh-keys = "0.45.0" | ||||
| rand = "0.8.5" | ||||
|  | ||||
| @ -6,7 +6,6 @@ use log::{debug, info, warn}; | ||||
| use tokio::sync::OnceCell; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::Id, | ||||
|     executors::ExecutorError, | ||||
|     interpret::{InterpretError, Outcome}, | ||||
|     inventory::Inventory, | ||||
| @ -18,9 +17,7 @@ use crate::{ | ||||
| use super::{ | ||||
|     HelmCommand, K8sclient, Topology, | ||||
|     k8s::K8sClient, | ||||
|     tenant::{ | ||||
|         ResourceLimits, TenantConfig, TenantManager, TenantNetworkPolicy, k8s::K8sTenantManager, | ||||
|     }, | ||||
|     tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, | ||||
| }; | ||||
| 
 | ||||
| struct K8sState { | ||||
|  | ||||
| @ -6,9 +6,14 @@ use crate::{ | ||||
| }; | ||||
| use async_trait::async_trait; | ||||
| use derive_new::new; | ||||
| use k8s_openapi::api::{ | ||||
|     core::v1::{Namespace, ResourceQuota}, | ||||
|     networking::v1::NetworkPolicy, | ||||
| use k8s_openapi::{ | ||||
|     api::{ | ||||
|         core::v1::{Namespace, ResourceQuota}, | ||||
|         networking::v1::{ | ||||
|             NetworkPolicy, NetworkPolicyEgressRule, NetworkPolicyIngressRule, NetworkPolicyPort, | ||||
|         }, | ||||
|     }, | ||||
|     apimachinery::pkg::util::intstr::IntOrString, | ||||
| }; | ||||
| use kube::Resource; | ||||
| use log::{debug, info, warn}; | ||||
| @ -191,12 +196,107 @@ impl K8sTenantManager { | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         serde_json::from_value(network_policy).map_err(|e| { | ||||
|             ExecutorError::ConfigurationError(format!( | ||||
|                 "Could not build TenantManager NetworkPolicy. {}", | ||||
|                 e | ||||
|             )) | ||||
|         }) | ||||
|         let mut network_policy: NetworkPolicy = | ||||
|             serde_json::from_value(network_policy).map_err(|e| { | ||||
|                 ExecutorError::ConfigurationError(format!( | ||||
|                     "Could not build TenantManager NetworkPolicy. {}", | ||||
|                     e | ||||
|                 )) | ||||
|             })?; | ||||
| 
 | ||||
|         config | ||||
|             .network_policy | ||||
|             .additional_allowed_cidr_ingress | ||||
|             .iter() | ||||
|             .try_for_each(|c| -> Result<(), ExecutorError> { | ||||
|                 let cidr_list: Vec<serde_json::Value> = | ||||
|                     c.0.iter() | ||||
|                         .map(|ci| { | ||||
|                             json!({ | ||||
|                             "ipBlock": { | ||||
|                               "cidr": ci.to_string(), | ||||
|                                } | ||||
|                             }) | ||||
|                         }) | ||||
|                         .collect(); | ||||
|                 let rule = serde_json::from_value::<NetworkPolicyIngressRule>(json!({ | ||||
|                     "from": cidr_list | ||||
|                 })) | ||||
|                 .map_err(|e| { | ||||
|                     ExecutorError::ConfigurationError(format!( | ||||
|                         "Could not build TenantManager NetworkPolicyIngressRule. {}", | ||||
|                         e | ||||
|                     )) | ||||
|                 })?; | ||||
| 
 | ||||
|                 network_policy | ||||
|                     .spec | ||||
|                     .as_mut() | ||||
|                     .unwrap() | ||||
|                     .ingress | ||||
|                     .as_mut() | ||||
|                     .unwrap() | ||||
|                     .push(rule); | ||||
|                 Ok(()) | ||||
|             })?; | ||||
| 
 | ||||
|         config | ||||
|             .network_policy | ||||
|             .additional_allowed_cidr_egress | ||||
|             .iter() | ||||
|             .try_for_each(|c| -> Result<(), ExecutorError> { | ||||
|                 let cidr_list: Vec<serde_json::Value> = | ||||
|                     c.0.iter() | ||||
|                         .map(|ci| { | ||||
|                             json!({ | ||||
|                             "ipBlock": { | ||||
|                               "cidr": ci.to_string(), | ||||
|                                } | ||||
|                             }) | ||||
|                         }) | ||||
|                         .collect(); | ||||
|                 let ports: Option<Vec<NetworkPolicyPort>> = | ||||
|                     c.1.as_ref().map(|spec| match &spec.data { | ||||
|                         super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort { | ||||
|                             port: Some(IntOrString::Int(port.clone().into())), | ||||
|                             ..Default::default() | ||||
|                         }], | ||||
|                         super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort { | ||||
|                             port: Some(IntOrString::Int(start.clone().into())), | ||||
|                             end_port: Some(end.clone().into()), | ||||
|                             protocol: None, // Not currently supported by Harmony
 | ||||
|                         }], | ||||
| 
 | ||||
|                         super::PortSpecData::ListOfPorts(items) => items | ||||
|                             .iter() | ||||
|                             .map(|i| NetworkPolicyPort { | ||||
|                                 port: Some(IntOrString::Int(i.clone().into())), | ||||
|                                 ..Default::default() | ||||
|                             }) | ||||
|                             .collect(), | ||||
|                     }); | ||||
|                 let rule = serde_json::from_value::<NetworkPolicyEgressRule>(json!({ | ||||
|                     "to": cidr_list, | ||||
|                     "ports": ports, | ||||
|                 })) | ||||
|                 .map_err(|e| { | ||||
|                     ExecutorError::ConfigurationError(format!( | ||||
|                         "Could not build TenantManager NetworkPolicyEgressRule. {}", | ||||
|                         e | ||||
|                     )) | ||||
|                 })?; | ||||
|                 network_policy | ||||
|                     .spec | ||||
|                     .as_mut() | ||||
|                     .unwrap() | ||||
|                     .egress | ||||
|                     .as_mut() | ||||
|                     .unwrap() | ||||
|                     .push(rule); | ||||
|                 Ok(()) | ||||
|             })?; | ||||
| 
 | ||||
|         Ok(network_policy) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| pub mod k8s; | ||||
| mod manager; | ||||
| use std::str::FromStr; | ||||
| 
 | ||||
| pub use manager::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| @ -27,22 +29,18 @@ impl Default for TenantConfig { | ||||
|         Self { | ||||
|             name: format!("tenant_{id}"), | ||||
|             id, | ||||
|             resource_limits: ResourceLimits { | ||||
|                 cpu_request_cores: 4.0, | ||||
|                 cpu_limit_cores: 4.0, | ||||
|                 memory_request_gb: 4.0, | ||||
|                 memory_limit_gb: 4.0, | ||||
|                 storage_total_gb: 20.0, | ||||
|             }, | ||||
|             resource_limits: ResourceLimits::default(), | ||||
|             network_policy: TenantNetworkPolicy { | ||||
|                 default_inter_tenant_ingress: InterTenantIngressPolicy::DenyAll, | ||||
|                 default_internet_egress: InternetEgressPolicy::AllowAll, | ||||
|                 additional_allowed_cidr_ingress: vec![], | ||||
|                 additional_allowed_cidr_egress: vec![], | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] | ||||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||||
| pub struct ResourceLimits { | ||||
|     /// Requested/guaranteed CPU cores (e.g., 2.0).
 | ||||
|     pub cpu_request_cores: f32, | ||||
| @ -58,6 +56,18 @@ pub struct ResourceLimits { | ||||
|     pub storage_total_gb: f32, | ||||
| } | ||||
| 
 | ||||
| impl Default for ResourceLimits { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             cpu_request_cores: 4.0, | ||||
|             cpu_limit_cores: 4.0, | ||||
|             memory_request_gb: 4.0, | ||||
|             memory_limit_gb: 4.0, | ||||
|             storage_total_gb: 20.0, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||||
| pub struct TenantNetworkPolicy { | ||||
|     /// Policy for ingress traffic originating from other tenants within the same Harmony-managed environment.
 | ||||
| @ -65,6 +75,20 @@ pub struct TenantNetworkPolicy { | ||||
| 
 | ||||
|     /// Policy for egress traffic destined for the public internet.
 | ||||
|     pub default_internet_egress: InternetEgressPolicy, | ||||
| 
 | ||||
|     pub additional_allowed_cidr_ingress: Vec<(Vec<cidr::Ipv4Cidr>, Option<PortSpec>)>, | ||||
|     pub additional_allowed_cidr_egress: Vec<(Vec<cidr::Ipv4Cidr>, Option<PortSpec>)>, | ||||
| } | ||||
| 
 | ||||
| impl Default for TenantNetworkPolicy { | ||||
|     fn default() -> Self { | ||||
|         TenantNetworkPolicy { | ||||
|             default_inter_tenant_ingress: InterTenantIngressPolicy::DenyAll, | ||||
|             default_internet_egress: InternetEgressPolicy::DenyAll, | ||||
|             additional_allowed_cidr_ingress: vec![], | ||||
|             additional_allowed_cidr_egress: vec![], | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||||
| @ -80,3 +104,121 @@ pub enum InternetEgressPolicy { | ||||
|     /// Deny all outbound traffic to the internet by default.
 | ||||
|     DenyAll, | ||||
| } | ||||
| 
 | ||||
| /// Represents a port specification that can be either a single port, a comma-separated list of ports,
 | ||||
| /// or a range separated by a dash.
 | ||||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||||
| pub struct PortSpec { | ||||
|     /// The actual representation of the ports as strings for serialization/deserialization purposes.
 | ||||
|     pub data: PortSpecData, | ||||
| } | ||||
| 
 | ||||
| impl PortSpec { | ||||
|     /// TODO write short rust doc that shows what types of input are supported
 | ||||
|     fn parse_from_str(spec: &str) -> Result<PortSpec, String> { | ||||
|         // Check for single port
 | ||||
|         if let Ok(port) = spec.parse::<u16>() { | ||||
|             let spec = PortSpecData::SinglePort(port); | ||||
|             return Ok(Self { data: spec }); | ||||
|         } | ||||
| 
 | ||||
|         if let Some(range) = spec.find('-') { | ||||
|             let start_str = &spec[..range]; | ||||
|             let end_str = &spec[(range + 1)..]; | ||||
| 
 | ||||
|             if let (Ok(start), Ok(end)) = (start_str.parse::<u16>(), end_str.parse::<u16>()) { | ||||
|                 let spec = PortSpecData::PortRange(start, end); | ||||
|                 return Ok(Self { data: spec }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let ports: Vec<&str> = spec.split(',').collect(); | ||||
|         if !ports.is_empty() && ports.iter().all(|p| p.parse::<u16>().is_ok()) { | ||||
|             let maybe_ports = ports.iter().try_fold(vec![], |mut list, &p| { | ||||
|                 if let Ok(p) = p.parse::<u16>() { | ||||
|                     list.push(p); | ||||
|                     return Ok(list); | ||||
|                 } | ||||
|                 Err(()) | ||||
|             }); | ||||
| 
 | ||||
|             if let Ok(ports) = maybe_ports { | ||||
|                 let spec = PortSpecData::ListOfPorts(ports); | ||||
|                 return Ok(Self { data: spec }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         Err(format!("Invalid port spec format {spec}")) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromStr for PortSpec { | ||||
|     type Err = String; | ||||
| 
 | ||||
|     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||
|         Self::parse_from_str(s) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||||
| pub enum PortSpecData { | ||||
|     SinglePort(u16), | ||||
|     PortRange(u16, u16), | ||||
|     ListOfPorts(Vec<u16>), | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_single_port() { | ||||
|         let port_spec = "2144".parse::<PortSpec>().unwrap(); | ||||
|         match port_spec.data { | ||||
|             PortSpecData::SinglePort(port) => assert_eq!(port, 2144), | ||||
|             _ => panic!("Expected SinglePort"), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_port_range() { | ||||
|         let port_spec = "80-90".parse::<PortSpec>().unwrap(); | ||||
|         match port_spec.data { | ||||
|             PortSpecData::PortRange(start, end) => { | ||||
|                 assert_eq!(start, 80); | ||||
|                 assert_eq!(end, 90); | ||||
|             } | ||||
|             _ => panic!("Expected PortRange"), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_list_of_ports() { | ||||
|         let port_spec = "2144,3424".parse::<PortSpec>().unwrap(); | ||||
|         match port_spec.data { | ||||
|             PortSpecData::ListOfPorts(ports) => { | ||||
|                 assert_eq!(ports[0], 2144); | ||||
|                 assert_eq!(ports[1], 3424); | ||||
|             } | ||||
|             _ => panic!("Expected ListOfPorts"), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_invalid_port_spec() { | ||||
|         let result = "invalid".parse::<PortSpec>(); | ||||
|         assert!(result.is_err()); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_empty_input() { | ||||
|         let result = "".parse::<PortSpec>(); | ||||
|         assert!(result.is_err()); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_only_coma() { | ||||
|         let result = ",".parse::<PortSpec>(); | ||||
|         assert!(result.is_err()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,7 @@ | ||||
| use super::{config::KubePrometheusConfig, monitoring_alerting::AlertChannel}; | ||||
| use log::info; | ||||
| use non_blank_string_rs::NonBlankString; | ||||
| use std::{collections::HashMap, str::FromStr}; | ||||
| use url::Url; | ||||
| use std::str::FromStr; | ||||
| 
 | ||||
| use crate::modules::helm::chart::HelmChartScore; | ||||
| 
 | ||||
|  | ||||
| @ -13,10 +13,7 @@ use crate::{ | ||||
|     topology::{HelmCommand, Topology}, | ||||
| }; | ||||
| 
 | ||||
| use super::{ | ||||
|     config::KubePrometheusConfig, discord_alert_manager::discord_alert_manager_score, | ||||
|     kube_prometheus::kube_prometheus_helm_chart_score, | ||||
| }; | ||||
| use super::{config::KubePrometheusConfig, kube_prometheus::kube_prometheus_helm_chart_score}; | ||||
| 
 | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub enum AlertChannel { | ||||
|  | ||||
| @ -14,6 +14,7 @@ quote = "1.0.37" | ||||
| serde = "1.0.217" | ||||
| serde_yaml = "0.9.34" | ||||
| syn = "2.0.90" | ||||
| cidr.workspace = true | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| serde = { version = "1.0.217", features = ["derive"] } | ||||
|  | ||||
| @ -132,3 +132,16 @@ pub fn ingress_path(input: TokenStream) -> TokenStream { | ||||
|         false => panic!("Invalid ingress path"), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[proc_macro] | ||||
| pub fn cidrv4(input: TokenStream) -> TokenStream { | ||||
|     let input = parse_macro_input!(input as LitStr); | ||||
|     let cidr_str = input.value(); | ||||
| 
 | ||||
|     if let Ok(_) = cidr_str.parse::<cidr::Ipv4Cidr>() { | ||||
|         let expanded = quote! { #cidr_str.parse::<cidr::Ipv4Cidr>().unwrap() }; | ||||
|         return TokenStream::from(expanded); | ||||
|     } | ||||
| 
 | ||||
|     panic!("Invalid IPv4 CIDR : {}", cidr_str); | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user