1use serde_json::Value;
6
7#[allow(missing_docs)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum YamlType {
15 K8sDeployment,
17 K8sService,
18 K8sConfigMap,
19 K8sSecret,
20 K8sIngress,
21 K8sHPA,
22 K8sCronJob,
23 K8sJob,
24 K8sPVC,
25 K8sNetworkPolicy,
26 K8sStatefulSet,
27 K8sDaemonSet,
28 K8sRole,
29 K8sClusterRole,
30 K8sRoleBinding,
31 K8sClusterRoleBinding,
32 K8sServiceAccount,
33 K8sGeneric,
34
35 GitLabCI,
37 GitHubActions,
38
39 DockerCompose,
41
42 Prometheus,
44 Alertmanager,
45
46 HelmValues,
48 Ansible,
49 OpenAPI,
50
51 Generic,
53}
54
55impl YamlType {
56 pub fn to_schema_key(&self) -> &'static str {
58 match self {
59 YamlType::K8sDeployment => "k8s/deployment",
60 YamlType::K8sService => "k8s/service",
61 YamlType::K8sConfigMap => "k8s/configmap",
62 YamlType::K8sSecret => "k8s/secret",
63 YamlType::K8sIngress => "k8s/ingress",
64 YamlType::K8sHPA => "k8s/horizontalpodautoscaler",
65 YamlType::K8sCronJob => "k8s/cronjob",
66 YamlType::K8sJob => "k8s/job",
67 YamlType::K8sPVC => "k8s/persistentvolumeclaim",
68 YamlType::K8sNetworkPolicy => "k8s/networkpolicy",
69 YamlType::K8sStatefulSet => "k8s/statefulset",
70 YamlType::K8sDaemonSet => "k8s/daemonset",
71 YamlType::K8sRole => "k8s/role",
72 YamlType::K8sClusterRole => "k8s/clusterrole",
73 YamlType::K8sRoleBinding => "k8s/rolebinding",
74 YamlType::K8sClusterRoleBinding => "k8s/clusterrolebinding",
75 YamlType::K8sServiceAccount => "k8s/serviceaccount",
76 YamlType::K8sGeneric => "k8s/generic",
77
78 YamlType::GitLabCI => "gitlab-ci",
79 YamlType::GitHubActions => "github-actions",
80 YamlType::DockerCompose => "docker-compose",
81 YamlType::Prometheus => "prometheus",
82 YamlType::Alertmanager => "alertmanager",
83 YamlType::HelmValues => "helm-values",
84 YamlType::Ansible => "ansible",
85 YamlType::OpenAPI => "openapi",
86
87 YamlType::Generic => "generic",
88 }
89 }
90
91 pub fn is_kubernetes(&self) -> bool {
93 matches!(
94 self,
95 YamlType::K8sDeployment
96 | YamlType::K8sService
97 | YamlType::K8sConfigMap
98 | YamlType::K8sSecret
99 | YamlType::K8sIngress
100 | YamlType::K8sHPA
101 | YamlType::K8sCronJob
102 | YamlType::K8sJob
103 | YamlType::K8sPVC
104 | YamlType::K8sNetworkPolicy
105 | YamlType::K8sStatefulSet
106 | YamlType::K8sDaemonSet
107 | YamlType::K8sRole
108 | YamlType::K8sClusterRole
109 | YamlType::K8sRoleBinding
110 | YamlType::K8sClusterRoleBinding
111 | YamlType::K8sServiceAccount
112 | YamlType::K8sGeneric
113 )
114 }
115
116 pub fn display_name(&self) -> &'static str {
118 match self {
119 YamlType::K8sDeployment => "Kubernetes Deployment",
120 YamlType::K8sService => "Kubernetes Service",
121 YamlType::K8sConfigMap => "Kubernetes ConfigMap",
122 YamlType::K8sSecret => "Kubernetes Secret",
123 YamlType::K8sIngress => "Kubernetes Ingress",
124 YamlType::K8sHPA => "Kubernetes HPA",
125 YamlType::K8sCronJob => "Kubernetes CronJob",
126 YamlType::K8sJob => "Kubernetes Job",
127 YamlType::K8sPVC => "Kubernetes PVC",
128 YamlType::K8sNetworkPolicy => "Kubernetes NetworkPolicy",
129 YamlType::K8sStatefulSet => "Kubernetes StatefulSet",
130 YamlType::K8sDaemonSet => "Kubernetes DaemonSet",
131 YamlType::K8sRole => "Kubernetes Role",
132 YamlType::K8sClusterRole => "Kubernetes ClusterRole",
133 YamlType::K8sRoleBinding => "Kubernetes RoleBinding",
134 YamlType::K8sClusterRoleBinding => "Kubernetes ClusterRoleBinding",
135 YamlType::K8sServiceAccount => "Kubernetes ServiceAccount",
136 YamlType::K8sGeneric => "Kubernetes Resource",
137 YamlType::GitLabCI => "GitLab CI",
138 YamlType::GitHubActions => "GitHub Actions",
139 YamlType::DockerCompose => "Docker Compose",
140 YamlType::Prometheus => "Prometheus Config",
141 YamlType::Alertmanager => "Alertmanager Config",
142 YamlType::HelmValues => "Helm Values",
143 YamlType::Ansible => "Ansible Playbook",
144 YamlType::OpenAPI => "OpenAPI Spec",
145 YamlType::Generic => "Generic YAML",
146 }
147 }
148}
149
150impl std::fmt::Display for YamlType {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 write!(f, "{}", self.display_name())
153 }
154}
155
156pub fn detect_type(data: &Value) -> YamlType {
164 let obj = match data.as_object() {
165 Some(o) => o,
166 None => {
167 if data.as_array().is_some() && looks_like_ansible(data) {
169 return YamlType::Ansible;
170 }
171 return YamlType::Generic;
172 }
173 };
174
175 if let Some(schema_url) = obj.get("$schema").and_then(|s| s.as_str()) {
177 return detect_type_from_schema_url(schema_url);
178 }
179
180 if let (Some(_api_version), Some(kind)) = (
182 obj.get("apiVersion").and_then(|v| v.as_str()),
183 obj.get("kind").and_then(|v| v.as_str()),
184 ) {
185 return match kind {
186 "Deployment" => YamlType::K8sDeployment,
187 "Service" => YamlType::K8sService,
188 "ConfigMap" => YamlType::K8sConfigMap,
189 "Secret" => YamlType::K8sSecret,
190 "Ingress" => YamlType::K8sIngress,
191 "HorizontalPodAutoscaler" => YamlType::K8sHPA,
192 "CronJob" => YamlType::K8sCronJob,
193 "Job" => YamlType::K8sJob,
194 "PersistentVolumeClaim" => YamlType::K8sPVC,
195 "NetworkPolicy" => YamlType::K8sNetworkPolicy,
196 "StatefulSet" => YamlType::K8sStatefulSet,
197 "DaemonSet" => YamlType::K8sDaemonSet,
198 "Role" => YamlType::K8sRole,
199 "ClusterRole" => YamlType::K8sClusterRole,
200 "RoleBinding" => YamlType::K8sRoleBinding,
201 "ClusterRoleBinding" => YamlType::K8sClusterRoleBinding,
202 "ServiceAccount" => YamlType::K8sServiceAccount,
203 _ => YamlType::K8sGeneric,
204 };
205 }
206
207 if obj.contains_key("stages") || obj.values().any(|v| v.get("script").is_some()) {
209 return YamlType::GitLabCI;
210 }
211
212 if obj.contains_key("on") && obj.contains_key("jobs") {
214 return YamlType::GitHubActions;
215 }
216
217 if let Some(services) = obj.get("services").and_then(|s| s.as_object())
219 && !services.is_empty()
220 && services.values().any(|v| v.is_object())
221 {
222 return YamlType::DockerCompose;
223 }
224
225 if obj.contains_key("scrape_configs")
227 || obj
228 .get("global")
229 .and_then(|g| g.get("scrape_interval"))
230 .is_some()
231 {
232 return YamlType::Prometheus;
233 }
234
235 if obj.contains_key("route") && obj.contains_key("receivers") {
237 return YamlType::Alertmanager;
238 }
239
240 if looks_like_helm_values(obj) {
242 return YamlType::HelmValues;
243 }
244
245 if obj.contains_key("openapi") || obj.contains_key("swagger") {
247 return YamlType::OpenAPI;
248 }
249
250 YamlType::Generic
251}
252
253fn detect_type_from_schema_url(url: &str) -> YamlType {
255 if url.contains("kubernetesjsonschema.dev") || url.contains("kubernetes") {
256 YamlType::K8sGeneric
257 } else if url.contains("gitlab-ci") {
258 YamlType::GitLabCI
259 } else if url.contains("github-workflow") || url.contains("github-actions") {
260 YamlType::GitHubActions
261 } else if url.contains("docker-compose") {
262 YamlType::DockerCompose
263 } else if url.contains("prometheus") {
264 YamlType::Prometheus
265 } else if url.contains("alertmanager") {
266 YamlType::Alertmanager
267 } else if url.contains("openapi") || url.contains("swagger") {
268 YamlType::OpenAPI
269 } else {
270 YamlType::Generic
271 }
272}
273
274fn looks_like_ansible(data: &Value) -> bool {
276 data.as_array()
277 .map(|arr| {
278 arr.iter().all(|item| {
279 item.get("hosts").is_some()
280 || item.get("tasks").is_some()
281 || item.get("roles").is_some()
282 })
283 })
284 .unwrap_or(false)
285}
286
287fn looks_like_helm_values(obj: &serde_json::Map<String, Value>) -> bool {
289 let has_common_helm_keys = obj.contains_key("image")
291 || obj.contains_key("replicaCount")
292 || obj.contains_key("service")
293 && obj.get("service").and_then(|s| s.get("type")).is_some();
294
295 let no_k8s_markers = !obj.contains_key("apiVersion") && !obj.contains_key("kind");
297
298 has_common_helm_keys && no_k8s_markers
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use serde_json::json;
305
306 #[test]
307 fn test_detect_k8s_deployment() {
308 let data = json!({
309 "apiVersion": "apps/v1",
310 "kind": "Deployment",
311 "metadata": { "name": "test" },
312 "spec": {}
313 });
314 assert_eq!(detect_type(&data), YamlType::K8sDeployment);
315 }
316
317 #[test]
318 fn test_detect_k8s_service() {
319 let data = json!({
320 "apiVersion": "v1",
321 "kind": "Service",
322 "metadata": { "name": "test" }
323 });
324 assert_eq!(detect_type(&data), YamlType::K8sService);
325 }
326
327 #[test]
328 fn test_detect_gitlab_ci() {
329 let data = json!({
330 "stages": ["build", "test"],
331 "build_job": { "script": ["echo hello"] }
332 });
333 assert_eq!(detect_type(&data), YamlType::GitLabCI);
334 }
335
336 #[test]
337 fn test_detect_github_actions() {
338 let data = json!({
339 "on": ["push"],
340 "jobs": { "build": {} }
341 });
342 assert_eq!(detect_type(&data), YamlType::GitHubActions);
343 }
344
345 #[test]
346 fn test_detect_docker_compose() {
347 let data = json!({
348 "services": {
349 "web": { "image": "nginx" }
350 }
351 });
352 assert_eq!(detect_type(&data), YamlType::DockerCompose);
353 }
354
355 #[test]
356 fn test_detect_prometheus() {
357 let data = json!({
358 "global": { "scrape_interval": "15s" },
359 "scrape_configs": []
360 });
361 assert_eq!(detect_type(&data), YamlType::Prometheus);
362 }
363
364 #[test]
365 fn test_detect_alertmanager() {
366 let data = json!({
367 "route": { "receiver": "default" },
368 "receivers": [{ "name": "default" }]
369 });
370 assert_eq!(detect_type(&data), YamlType::Alertmanager);
371 }
372
373 #[test]
374 fn test_detect_openapi() {
375 let data = json!({
376 "openapi": "3.0.0",
377 "info": { "title": "API", "version": "1.0" }
378 });
379 assert_eq!(detect_type(&data), YamlType::OpenAPI);
380 }
381
382 #[test]
383 fn test_schema_key_conversion() {
384 assert_eq!(YamlType::K8sDeployment.to_schema_key(), "k8s/deployment");
385 assert_eq!(YamlType::GitLabCI.to_schema_key(), "gitlab-ci");
386 assert_eq!(YamlType::DockerCompose.to_schema_key(), "docker-compose");
387 }
388}