1use serde::{Deserialize, Serialize};
2use crate::models::k8s::K8sMetadata;
3use crate::models::validation::{ConfigValidator, Diagnostic, Severity, YamlType};
4
5#[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 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 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#[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}