use serde::{Deserialize, Serialize};
use crate::models::k8s::K8sMetadata;
use crate::models::validation::{ConfigValidator, Diagnostic, Severity, YamlType};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IngressTLS {
#[serde(default)]
pub hosts: Vec<String>,
#[serde(default, rename = "secretName")]
pub secret_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IngressBackend {
#[serde(default)]
pub service: Option<IngressServiceBackend>,
#[serde(default)]
pub resource: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IngressServiceBackend {
pub name: String,
pub port: IngressServicePort,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IngressServicePort {
#[serde(default)]
pub number: Option<u16>,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IngressPath {
pub path: String,
#[serde(default, rename = "pathType")]
pub path_type: Option<String>,
pub backend: IngressBackend,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IngressHTTP {
pub paths: Vec<IngressPath>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IngressRule {
#[serde(default)]
pub host: Option<String>,
#[serde(default)]
pub http: Option<IngressHTTP>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IngressSpec {
#[serde(default, rename = "ingressClassName")]
pub ingress_class_name: Option<String>,
#[serde(default)]
pub tls: Vec<IngressTLS>,
#[serde(default)]
pub rules: Vec<IngressRule>,
#[serde(default, rename = "defaultBackend")]
pub default_backend: Option<IngressBackend>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sIngress {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
pub spec: IngressSpec,
}
impl ConfigValidator for K8sIngress {
fn yaml_type(&self) -> YamlType {
YamlType::K8sIngress
}
fn validate_structure(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
if self.spec.rules.is_empty() && self.spec.default_backend.is_none() {
diags.push(Diagnostic {
severity: Severity::Error,
message: "Ingress has no rules and no defaultBackend — no traffic will be routed".into(),
path: Some("spec".into()),
});
}
for (i, rule) in self.spec.rules.iter().enumerate() {
if let Some(http) = &rule.http {
for (j, p) in http.paths.iter().enumerate() {
if p.backend.service.is_none() && p.backend.resource.is_none() {
diags.push(Diagnostic {
severity: Severity::Error,
message: format!("Rule[{}].path[{}] '{}' has no backend", i, j, p.path),
path: Some(format!("spec > rules > {} > http > paths > {}", i, j)),
});
}
}
}
}
diags
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
let hosts_with_tls: Vec<&str> = self.spec.tls.iter()
.flat_map(|t| t.hosts.iter().map(|h| h.as_str()))
.collect();
for rule in &self.spec.rules {
if let Some(host) = &rule.host
&& !hosts_with_tls.contains(&host.as_str()) && !self.spec.tls.is_empty() {
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("Host '{}' has no TLS configuration", host),
path: Some("spec > tls".into()),
});
}
}
if self.spec.tls.is_empty() {
diags.push(Diagnostic {
severity: Severity::Info,
message: "No TLS configured — traffic will be unencrypted".into(),
path: Some("spec > tls".into()),
});
}
if self.spec.ingress_class_name.is_none() {
diags.push(Diagnostic {
severity: Severity::Info,
message: "No ingressClassName specified — cluster default will be used".into(),
path: Some("spec > ingressClassName".into()),
});
}
for (i, rule) in self.spec.rules.iter().enumerate() {
if let Some(http) = &rule.http {
for (j, p) in http.paths.iter().enumerate() {
if p.path_type.is_none() {
diags.push(Diagnostic {
severity: Severity::Warning,
message: format!("Rule[{}].path[{}] has no pathType — ImplementationSpecific will be used", i, j),
path: Some(format!("spec > rules > {} > http > paths > {} > pathType", i, j)),
});
}
}
}
}
diags
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkPolicyPort {
#[serde(default)]
pub port: Option<serde_json::Value>,
#[serde(default)]
pub protocol: Option<String>,
#[serde(default, rename = "endPort")]
pub end_port: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkPolicyPeer {
#[serde(default, rename = "podSelector")]
pub pod_selector: Option<serde_json::Value>,
#[serde(default, rename = "namespaceSelector")]
pub namespace_selector: Option<serde_json::Value>,
#[serde(default, rename = "ipBlock")]
pub ip_block: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkPolicyIngressRule {
#[serde(default)]
pub from: Vec<NetworkPolicyPeer>,
#[serde(default)]
pub ports: Vec<NetworkPolicyPort>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkPolicyEgressRule {
#[serde(default)]
pub to: Vec<NetworkPolicyPeer>,
#[serde(default)]
pub ports: Vec<NetworkPolicyPort>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkPolicySpec {
#[serde(rename = "podSelector")]
pub pod_selector: serde_json::Value,
#[serde(default, rename = "policyTypes")]
pub policy_types: Vec<String>,
#[serde(default)]
pub ingress: Vec<NetworkPolicyIngressRule>,
#[serde(default)]
pub egress: Vec<NetworkPolicyEgressRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sNetworkPolicy {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: K8sMetadata,
pub spec: NetworkPolicySpec,
}
impl ConfigValidator for K8sNetworkPolicy {
fn yaml_type(&self) -> YamlType {
YamlType::K8sNetworkPolicy
}
fn validate_structure(&self) -> Vec<Diagnostic> {
vec![]
}
fn validate_semantics(&self) -> Vec<Diagnostic> {
let mut diags = Vec::new();
let has_ingress_type = self.spec.policy_types.iter().any(|t| t == "Ingress");
let has_egress_type = self.spec.policy_types.iter().any(|t| t == "Egress");
if has_ingress_type && self.spec.ingress.is_empty() {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "policyTypes includes 'Ingress' but no ingress rules — all ingress will be denied".into(),
path: Some("spec > ingress".into()),
});
}
if has_egress_type && self.spec.egress.is_empty() {
diags.push(Diagnostic {
severity: Severity::Warning,
message: "policyTypes includes 'Egress' but no egress rules — all egress will be denied (including DNS!)".into(),
path: Some("spec > egress".into()),
});
}
if self.spec.policy_types.is_empty() {
diags.push(Diagnostic {
severity: Severity::Info,
message: "No policyTypes specified — only Ingress will be enforced if ingress rules exist".into(),
path: Some("spec > policyTypes".into()),
});
}
diags
}
}