Skip to main content

devops_models/models/
k8s_networking.rs

1use serde::{Deserialize, Serialize};
2use crate::models::k8s::K8sMetadata;
3use crate::models::validation::{ConfigValidator, Diagnostic, Severity, YamlType};
4
5// ═══════════════════════════════════════════════════════════════════════════
6// Ingress
7// ═══════════════════════════════════════════════════════════════════════════
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(deny_unknown_fields)]
11pub struct IngressTLS {
12    #[serde(default)]
13    pub hosts: Vec<String>,
14    #[serde(default, rename = "secretName")]
15    pub secret_name: Option<String>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct IngressBackend {
21    #[serde(default)]
22    pub service: Option<IngressServiceBackend>,
23    #[serde(default)]
24    pub resource: Option<serde_json::Value>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct IngressServiceBackend {
30    pub name: String,
31    pub port: IngressServicePort,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct IngressServicePort {
37    #[serde(default)]
38    pub number: Option<u16>,
39    #[serde(default)]
40    pub name: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct IngressPath {
46    pub path: String,
47    #[serde(default, rename = "pathType")]
48    pub path_type: Option<String>,
49    pub backend: IngressBackend,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct IngressHTTP {
55    pub paths: Vec<IngressPath>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct IngressRule {
61    #[serde(default)]
62    pub host: Option<String>,
63    #[serde(default)]
64    pub http: Option<IngressHTTP>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69pub struct IngressSpec {
70    #[serde(default, rename = "ingressClassName")]
71    pub ingress_class_name: Option<String>,
72    #[serde(default)]
73    pub tls: Vec<IngressTLS>,
74    #[serde(default)]
75    pub rules: Vec<IngressRule>,
76    #[serde(default, rename = "defaultBackend")]
77    pub default_backend: Option<IngressBackend>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct K8sIngress {
83    #[serde(rename = "apiVersion")]
84    pub api_version: String,
85    pub kind: String,
86    pub metadata: K8sMetadata,
87    pub spec: IngressSpec,
88}
89
90impl ConfigValidator for K8sIngress {
91    fn yaml_type(&self) -> YamlType {
92        YamlType::K8sIngress
93    }
94
95    fn validate_structure(&self) -> Vec<Diagnostic> {
96        let mut diags = Vec::new();
97        if self.spec.rules.is_empty() && self.spec.default_backend.is_none() {
98            diags.push(Diagnostic {
99                severity: Severity::Error,
100                message: "Ingress has no rules and no defaultBackend — no traffic will be routed".into(),
101                path: Some("spec".into()),
102            });
103        }
104        for (i, rule) in self.spec.rules.iter().enumerate() {
105            if let Some(http) = &rule.http {
106                for (j, p) in http.paths.iter().enumerate() {
107                    if p.backend.service.is_none() && p.backend.resource.is_none() {
108                        diags.push(Diagnostic {
109                            severity: Severity::Error,
110                            message: format!("Rule[{}].path[{}] '{}' has no backend", i, j, p.path),
111                            path: Some(format!("spec > rules > {} > http > paths > {}", i, j)),
112                        });
113                    }
114                }
115            }
116        }
117        diags
118    }
119
120    fn validate_semantics(&self) -> Vec<Diagnostic> {
121        let mut diags = Vec::new();
122        // TLS warnings
123        let hosts_with_tls: Vec<&str> = self.spec.tls.iter()
124            .flat_map(|t| t.hosts.iter().map(|h| h.as_str()))
125            .collect();
126        for rule in &self.spec.rules {
127            if let Some(host) = &rule.host
128                && !hosts_with_tls.contains(&host.as_str()) && !self.spec.tls.is_empty() {
129                    diags.push(Diagnostic {
130                        severity: Severity::Warning,
131                        message: format!("Host '{}' has no TLS configuration", host),
132                        path: Some("spec > tls".into()),
133                    });
134                }
135        }
136        if self.spec.tls.is_empty() {
137            diags.push(Diagnostic {
138                severity: Severity::Info,
139                message: "No TLS configured — traffic will be unencrypted".into(),
140                path: Some("spec > tls".into()),
141            });
142        }
143        if self.spec.ingress_class_name.is_none() {
144            diags.push(Diagnostic {
145                severity: Severity::Info,
146                message: "No ingressClassName specified — cluster default will be used".into(),
147                path: Some("spec > ingressClassName".into()),
148            });
149        }
150        // pathType check
151        for (i, rule) in self.spec.rules.iter().enumerate() {
152            if let Some(http) = &rule.http {
153                for (j, p) in http.paths.iter().enumerate() {
154                    if p.path_type.is_none() {
155                        diags.push(Diagnostic {
156                            severity: Severity::Warning,
157                            message: format!("Rule[{}].path[{}] has no pathType — ImplementationSpecific will be used", i, j),
158                            path: Some(format!("spec > rules > {} > http > paths > {} > pathType", i, j)),
159                        });
160                    }
161                }
162            }
163        }
164        diags
165    }
166}
167
168// ═══════════════════════════════════════════════════════════════════════════
169// NetworkPolicy
170// ═══════════════════════════════════════════════════════════════════════════
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(deny_unknown_fields)]
174pub struct NetworkPolicyPort {
175    #[serde(default)]
176    pub port: Option<serde_json::Value>,
177    #[serde(default)]
178    pub protocol: Option<String>,
179    #[serde(default, rename = "endPort")]
180    pub end_port: Option<u16>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(deny_unknown_fields)]
185pub struct NetworkPolicyPeer {
186    #[serde(default, rename = "podSelector")]
187    pub pod_selector: Option<serde_json::Value>,
188    #[serde(default, rename = "namespaceSelector")]
189    pub namespace_selector: Option<serde_json::Value>,
190    #[serde(default, rename = "ipBlock")]
191    pub ip_block: Option<serde_json::Value>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(deny_unknown_fields)]
196pub struct NetworkPolicyIngressRule {
197    #[serde(default)]
198    pub from: Vec<NetworkPolicyPeer>,
199    #[serde(default)]
200    pub ports: Vec<NetworkPolicyPort>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(deny_unknown_fields)]
205pub struct NetworkPolicyEgressRule {
206    #[serde(default)]
207    pub to: Vec<NetworkPolicyPeer>,
208    #[serde(default)]
209    pub ports: Vec<NetworkPolicyPort>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213#[serde(deny_unknown_fields)]
214pub struct NetworkPolicySpec {
215    #[serde(rename = "podSelector")]
216    pub pod_selector: serde_json::Value,
217    #[serde(default, rename = "policyTypes")]
218    pub policy_types: Vec<String>,
219    #[serde(default)]
220    pub ingress: Vec<NetworkPolicyIngressRule>,
221    #[serde(default)]
222    pub egress: Vec<NetworkPolicyEgressRule>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226#[serde(deny_unknown_fields)]
227pub struct K8sNetworkPolicy {
228    #[serde(rename = "apiVersion")]
229    pub api_version: String,
230    pub kind: String,
231    pub metadata: K8sMetadata,
232    pub spec: NetworkPolicySpec,
233}
234
235impl ConfigValidator for K8sNetworkPolicy {
236    fn yaml_type(&self) -> YamlType {
237        YamlType::K8sNetworkPolicy
238    }
239
240    fn validate_structure(&self) -> Vec<Diagnostic> {
241        vec![]
242    }
243
244    fn validate_semantics(&self) -> Vec<Diagnostic> {
245        let mut diags = Vec::new();
246        let has_ingress_type = self.spec.policy_types.iter().any(|t| t == "Ingress");
247        let has_egress_type = self.spec.policy_types.iter().any(|t| t == "Egress");
248
249        if has_ingress_type && self.spec.ingress.is_empty() {
250            diags.push(Diagnostic {
251                severity: Severity::Warning,
252                message: "policyTypes includes 'Ingress' but no ingress rules — all ingress will be denied".into(),
253                path: Some("spec > ingress".into()),
254            });
255        }
256        if has_egress_type && self.spec.egress.is_empty() {
257            diags.push(Diagnostic {
258                severity: Severity::Warning,
259                message: "policyTypes includes 'Egress' but no egress rules — all egress will be denied (including DNS!)".into(),
260                path: Some("spec > egress".into()),
261            });
262        }
263        if self.spec.policy_types.is_empty() {
264            diags.push(Diagnostic {
265                severity: Severity::Info,
266                message: "No policyTypes specified — only Ingress will be enforced if ingress rules exist".into(),
267                path: Some("spec > policyTypes".into()),
268            });
269        }
270        diags
271    }
272}