diff --git a/Cargo.lock b/Cargo.lock index a2d8c40..3252aa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 8dd08bb..1512154 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 044729f..6742b5a 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -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 { diff --git a/harmony/src/domain/topology/tenant/k8s.rs b/harmony/src/domain/topology/tenant/k8s.rs index 93fc0c3..a03e8d7 100644 --- a/harmony/src/domain/topology/tenant/k8s.rs +++ b/harmony/src/domain/topology/tenant/k8s.rs @@ -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 = + c.0.iter() + .map(|ci| { + json!({ + "ipBlock": { + "cidr": ci.to_string(), + } + }) + }) + .collect(); + let rule = serde_json::from_value::(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 = + c.0.iter() + .map(|ci| { + json!({ + "ipBlock": { + "cidr": ci.to_string(), + } + }) + }) + .collect(); + let ports: Option> = + 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::(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) } } diff --git a/harmony/src/domain/topology/tenant/mod.rs b/harmony/src/domain/topology/tenant/mod.rs index 35326fb..e8d41dc 100644 --- a/harmony/src/domain/topology/tenant/mod.rs +++ b/harmony/src/domain/topology/tenant/mod.rs @@ -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, Option)>, + pub additional_allowed_cidr_egress: Vec<(Vec, Option)>, +} + +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 { + // Check for single port + if let Ok(port) = spec.parse::() { + 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::(), end_str.parse::()) { + 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::().is_ok()) { + let maybe_ports = ports.iter().try_fold(vec![], |mut list, &p| { + if let Ok(p) = p.parse::() { + 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::parse_from_str(s) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum PortSpecData { + SinglePort(u16), + PortRange(u16, u16), + ListOfPorts(Vec), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_single_port() { + let port_spec = "2144".parse::().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::().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::().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::(); + assert!(result.is_err()); + } + + #[test] + fn test_empty_input() { + let result = "".parse::(); + assert!(result.is_err()); + } + + #[test] + fn test_only_coma() { + let result = ",".parse::(); + assert!(result.is_err()); + } +} diff --git a/harmony/src/modules/monitoring/kube_prometheus.rs b/harmony/src/modules/monitoring/kube_prometheus.rs index b694f51..e26ded8 100644 --- a/harmony/src/modules/monitoring/kube_prometheus.rs +++ b/harmony/src/modules/monitoring/kube_prometheus.rs @@ -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; diff --git a/harmony/src/modules/monitoring/monitoring_alerting.rs b/harmony/src/modules/monitoring/monitoring_alerting.rs index 6d2db38..bdddb7d 100644 --- a/harmony/src/modules/monitoring/monitoring_alerting.rs +++ b/harmony/src/modules/monitoring/monitoring_alerting.rs @@ -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 { diff --git a/harmony_macros/Cargo.toml b/harmony_macros/Cargo.toml index 1a314c3..7185d0b 100644 --- a/harmony_macros/Cargo.toml +++ b/harmony_macros/Cargo.toml @@ -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"] } diff --git a/harmony_macros/src/lib.rs b/harmony_macros/src/lib.rs index 7e9ee47..7a2748d 100644 --- a/harmony_macros/src/lib.rs +++ b/harmony_macros/src/lib.rs @@ -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::() { + let expanded = quote! { #cidr_str.parse::().unwrap() }; + return TokenStream::from(expanded); + } + + panic!("Invalid IPv4 CIDR : {}", cidr_str); +}