feat: add support for custom CIDR ingress/egress rules (#60)
All checks were successful
Run Check Script / check (push) Successful in 1m53s
All checks were successful
Run Check Script / check (push) Successful in 1m53s
- Added `additional_allowed_cidr_ingress` and `additional_allowed_cidr_egress` fields to `TenantNetworkPolicy` to allow specifying custom CIDR blocks for network access. - Updated K8sTenantManager to parse and apply these CIDR rules to NetworkPolicy ingress and egress rules. - Added `cidr` dependency to `harmony_macros` and a custom proc macro `cidrv4` to easily parse CIDR strings. - Updated TenantConfig to default inter tenant and internet egress to deny all and added default empty vectors for CIDR ingress and egress. - Updated ResourceLimits to implement default. Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/60 Co-authored-by: Jean-Gabriel Gill-Couture <jg@nationtech.io> Co-committed-by: Jean-Gabriel Gill-Couture <jg@nationtech.io>
This commit is contained in:
parent
ef5ec4a131
commit
b94dd1e595
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -394,6 +394,9 @@ name = "cidr"
|
|||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621"
|
checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
@ -1476,6 +1479,7 @@ dependencies = [
|
|||||||
name = "harmony_macros"
|
name = "harmony_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cidr",
|
||||||
"harmony_types",
|
"harmony_types",
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -24,7 +24,7 @@ env_logger = "0.11.5"
|
|||||||
derive-new = "0.7.0"
|
derive-new = "0.7.0"
|
||||||
async-trait = "0.1.82"
|
async-trait = "0.1.82"
|
||||||
tokio = { version = "1.40.0", features = ["io-std", "fs", "macros", "rt-multi-thread"] }
|
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 = "0.45.0"
|
||||||
russh-keys = "0.45.0"
|
russh-keys = "0.45.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
@ -6,7 +6,6 @@ use log::{debug, info, warn};
|
|||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::Id,
|
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
interpret::{InterpretError, Outcome},
|
interpret::{InterpretError, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
@ -18,9 +17,7 @@ use crate::{
|
|||||||
use super::{
|
use super::{
|
||||||
HelmCommand, K8sclient, Topology,
|
HelmCommand, K8sclient, Topology,
|
||||||
k8s::K8sClient,
|
k8s::K8sClient,
|
||||||
tenant::{
|
tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager},
|
||||||
ResourceLimits, TenantConfig, TenantManager, TenantNetworkPolicy, k8s::K8sTenantManager,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct K8sState {
|
struct K8sState {
|
||||||
|
@ -6,9 +6,14 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use k8s_openapi::api::{
|
use k8s_openapi::{
|
||||||
core::v1::{Namespace, ResourceQuota},
|
api::{
|
||||||
networking::v1::NetworkPolicy,
|
core::v1::{Namespace, ResourceQuota},
|
||||||
|
networking::v1::{
|
||||||
|
NetworkPolicy, NetworkPolicyEgressRule, NetworkPolicyIngressRule, NetworkPolicyPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apimachinery::pkg::util::intstr::IntOrString,
|
||||||
};
|
};
|
||||||
use kube::Resource;
|
use kube::Resource;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
@ -191,12 +196,107 @@ impl K8sTenantManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
serde_json::from_value(network_policy).map_err(|e| {
|
let mut network_policy: NetworkPolicy =
|
||||||
ExecutorError::ConfigurationError(format!(
|
serde_json::from_value(network_policy).map_err(|e| {
|
||||||
"Could not build TenantManager NetworkPolicy. {}",
|
ExecutorError::ConfigurationError(format!(
|
||||||
e
|
"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;
|
pub mod k8s;
|
||||||
mod manager;
|
mod manager;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -27,22 +29,18 @@ impl Default for TenantConfig {
|
|||||||
Self {
|
Self {
|
||||||
name: format!("tenant_{id}"),
|
name: format!("tenant_{id}"),
|
||||||
id,
|
id,
|
||||||
resource_limits: ResourceLimits {
|
resource_limits: ResourceLimits::default(),
|
||||||
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,
|
|
||||||
},
|
|
||||||
network_policy: TenantNetworkPolicy {
|
network_policy: TenantNetworkPolicy {
|
||||||
default_inter_tenant_ingress: InterTenantIngressPolicy::DenyAll,
|
default_inter_tenant_ingress: InterTenantIngressPolicy::DenyAll,
|
||||||
default_internet_egress: InternetEgressPolicy::AllowAll,
|
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 {
|
pub struct ResourceLimits {
|
||||||
/// Requested/guaranteed CPU cores (e.g., 2.0).
|
/// Requested/guaranteed CPU cores (e.g., 2.0).
|
||||||
pub cpu_request_cores: f32,
|
pub cpu_request_cores: f32,
|
||||||
@ -58,6 +56,18 @@ pub struct ResourceLimits {
|
|||||||
pub storage_total_gb: f32,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct TenantNetworkPolicy {
|
pub struct TenantNetworkPolicy {
|
||||||
/// Policy for ingress traffic originating from other tenants within the same Harmony-managed environment.
|
/// 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.
|
/// Policy for egress traffic destined for the public internet.
|
||||||
pub default_internet_egress: InternetEgressPolicy,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@ -80,3 +104,121 @@ pub enum InternetEgressPolicy {
|
|||||||
/// Deny all outbound traffic to the internet by default.
|
/// Deny all outbound traffic to the internet by default.
|
||||||
DenyAll,
|
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 super::{config::KubePrometheusConfig, monitoring_alerting::AlertChannel};
|
||||||
use log::info;
|
use log::info;
|
||||||
use non_blank_string_rs::NonBlankString;
|
use non_blank_string_rs::NonBlankString;
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::str::FromStr;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::modules::helm::chart::HelmChartScore;
|
use crate::modules::helm::chart::HelmChartScore;
|
||||||
|
|
||||||
|
@ -13,10 +13,7 @@ use crate::{
|
|||||||
topology::{HelmCommand, Topology},
|
topology::{HelmCommand, Topology},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{config::KubePrometheusConfig, kube_prometheus::kube_prometheus_helm_chart_score};
|
||||||
config::KubePrometheusConfig, discord_alert_manager::discord_alert_manager_score,
|
|
||||||
kube_prometheus::kube_prometheus_helm_chart_score,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub enum AlertChannel {
|
pub enum AlertChannel {
|
||||||
|
@ -14,6 +14,7 @@ quote = "1.0.37"
|
|||||||
serde = "1.0.217"
|
serde = "1.0.217"
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
syn = "2.0.90"
|
syn = "2.0.90"
|
||||||
|
cidr.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
|
@ -132,3 +132,16 @@ pub fn ingress_path(input: TokenStream) -> TokenStream {
|
|||||||
false => panic!("Invalid ingress path"),
|
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