use crate::core::Result;
use crate::engine::{Mission, MissionStep, StepType, MissionConfig};
use crate::transpiler::common::TranspilationContext;
use serde_json::{json, Value};
use std::collections::HashMap;
pub struct KubernetesParser;
#[derive(Debug, Clone)]
pub struct KubernetesResource {
pub api_version: String,
pub kind: String,
pub name: String,
pub namespace: Option<String>,
pub spec: HashMap<String, Value>,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct KubernetesSchedule {
pub schedule: Option<String>, pub job_template: Option<HashMap<String, Value>>,
pub parallelism: Option<i32>,
pub completions: Option<i32>,
}
impl KubernetesParser {
pub async fn parse_file(file_path: &str) -> Result<Mission> {
let content = std::fs::read_to_string(file_path)?;
Self::parse_string(&content).await
}
pub async fn parse_string(content: &str) -> Result<Mission> {
let mut context = TranspilationContext::new("Kubernetes Deployment Mission".to_string());
let resources = Self::parse_resources(content)?;
let mut steps = Vec::new();
let mut step_counter = 1;
let ordered_resources = Self::order_resources_by_dependencies(&resources)?;
for resource in &ordered_resources {
let step = Self::create_resource_step(resource, &format!("k8s_{}", step_counter))?;
steps.push(step);
step_counter += 1;
if matches!(resource.kind.as_str(), "Deployment" | "Service" | "StatefulSet") {
let health_step = Self::create_health_check_step(resource, &format!("health_{}", step_counter))?;
steps.push(health_step);
step_counter += 1;
}
}
context.add_variable("total_resources".to_string(), resources.len().to_string());
context.add_variable("resource_types".to_string(), Self::get_resource_types(&resources).join(","));
Ok(Mission {
version: "1.0".to_string(),
name: "Kubernetes Deployment Mission".to_string(),
description: Some(format!("Converted from Kubernetes manifest with {} resources: {}",
resources.len(),
Self::get_resource_types(&resources).join(", "))),
steps,
config: Some(MissionConfig {
max_parallel_steps: Some(2), timeout_seconds: Some(1800), fail_fast: Some(true), }),
})
}
fn parse_resources(content: &str) -> Result<Vec<KubernetesResource>> {
let mut resources = Vec::new();
let documents = content.split("---").filter(|doc| !doc.trim().is_empty());
for doc in documents {
if let Some(resource) = Self::parse_single_resource(doc)? {
resources.push(resource);
}
}
Ok(resources)
}
fn parse_single_resource(yaml_content: &str) -> Result<Option<KubernetesResource>> {
let lines: Vec<&str> = yaml_content.lines().map(|l| l.trim()).collect();
let mut api_version = String::new();
let mut kind = String::new();
let mut name = String::new();
let mut namespace = None;
let spec = HashMap::new();
for line in lines {
if line.starts_with("apiVersion:") {
api_version = line.replace("apiVersion:", "").trim().to_string();
} else if line.starts_with("kind:") {
kind = line.replace("kind:", "").trim().to_string();
} else if line.contains("name:") && name.is_empty() {
name = line.replace("name:", "").trim().to_string();
} else if line.contains("namespace:") {
namespace = Some(line.replace("namespace:", "").trim().to_string());
}
}
if api_version.is_empty() || kind.is_empty() || name.is_empty() {
return Ok(None);
}
Ok(Some(KubernetesResource {
api_version,
kind,
name,
namespace,
spec,
dependencies: Vec::new(),
}))
}
fn order_resources_by_dependencies(resources: &[KubernetesResource]) -> Result<Vec<KubernetesResource>> {
let mut ordered = Vec::new();
let order_priority = [
"Namespace",
"ConfigMap",
"Secret",
"PersistentVolume",
"PersistentVolumeClaim",
"ServiceAccount",
"Role",
"RoleBinding",
"Service",
"Deployment",
"StatefulSet",
"DaemonSet",
"Job",
"CronJob",
"Ingress",
];
for priority_kind in &order_priority {
for resource in resources {
if resource.kind == *priority_kind {
ordered.push(resource.clone());
}
}
}
for resource in resources {
if !ordered.iter().any(|r| r.name == resource.name && r.kind == resource.kind) {
ordered.push(resource.clone());
}
}
Ok(ordered)
}
fn get_resource_types(resources: &[KubernetesResource]) -> Vec<String> {
let mut types: Vec<String> = resources.iter()
.map(|r| r.kind.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
types.sort();
types
}
fn create_resource_step(resource: &KubernetesResource, step_id: &str) -> Result<MissionStep> {
let step_type = Self::map_resource_to_step_type(&resource.kind);
let (timeout, continue_on_error) = Self::get_step_characteristics(&resource.kind);
Ok(MissionStep {
id: step_id.to_string(),
name: format!("Deploy {} {}", resource.kind, resource.name),
step_type,
depends_on: if resource.dependencies.is_empty() { None } else { Some(resource.dependencies.clone()) },
timeout_seconds: Some(timeout),
continue_on_error: Some(continue_on_error),
parameters: json!({
"action": "apply",
"resource_type": resource.kind,
"resource_name": resource.name,
"api_version": resource.api_version,
"namespace": resource.namespace.clone().unwrap_or_else(|| "default".to_string()),
"kubernetes_resource": true,
"wait_for_ready": matches!(resource.kind.as_str(), "Deployment" | "StatefulSet" | "DaemonSet")
}),
})
}
fn create_health_check_step(resource: &KubernetesResource, step_id: &str) -> Result<MissionStep> {
Ok(MissionStep {
id: step_id.to_string(),
name: format!("Health Check {} {}", resource.kind, resource.name),
step_type: StepType::Http,
depends_on: Some(vec![format!("k8s_deploy_{}", resource.name)]),
timeout_seconds: Some(300), continue_on_error: Some(false), parameters: json!({
"action": "health_check",
"resource_type": resource.kind,
"resource_name": resource.name,
"namespace": resource.namespace.clone().unwrap_or_else(|| "default".to_string()),
"method": "GET",
"url": format!("http://{}.{}.svc.cluster.local/health",
resource.name,
resource.namespace.clone().unwrap_or_else(|| "default".to_string())),
"expected_status": 200,
"retry_count": 3,
"retry_delay": 10
}),
})
}
fn map_resource_to_step_type(kind: &str) -> StepType {
match kind {
"ConfigMap" | "Secret" => StepType::CreateFile, "Namespace" => StepType::Noop, "Job" => StepType::Command, "CronJob" => StepType::Noop, "Deployment" | "StatefulSet" | "DaemonSet" => StepType::Http, "Service" | "Ingress" => StepType::Http, "PersistentVolume" | "PersistentVolumeClaim" => StepType::Http, _ => StepType::Http, }
}
fn get_step_characteristics(kind: &str) -> (u64, bool) {
match kind {
"Namespace" | "ConfigMap" | "Secret" => (60, false), "Job" => (1800, true), "Deployment" | "StatefulSet" => (600, false), "Service" | "Ingress" => (120, false), "PersistentVolume" | "PersistentVolumeClaim" => (300, false), _ => (300, false), }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_parse_simple_deployment() {
let k8s_manifest = r#"
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
"#;
let result = KubernetesParser::parse_string(k8s_manifest).await;
assert!(result.is_ok());
let mission = result.unwrap();
assert_eq!(mission.name, "Kubernetes Deployment Mission");
assert!(mission.steps.len() >= 1);
let step_names: Vec<String> = mission.steps.iter().map(|s| s.name.clone()).collect();
assert!(step_names.iter().any(|name| name.contains("Deployment") && name.contains("nginx-deployment")));
}
#[tokio::test]
async fn test_parse_multi_resource_manifest() {
let k8s_manifest = r#"
apiVersion: v1
kind: Namespace
metadata:
name: test-namespace
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: test-namespace
data:
config.yml: |
app:
name: test-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
namespace: test-namespace
spec:
replicas: 2
"#;
let result = KubernetesParser::parse_string(k8s_manifest).await;
assert!(result.is_ok());
let mission = result.unwrap();
assert!(mission.steps.len() >= 3); assert!(mission.description.unwrap().contains("3 resources"));
let step_names: Vec<String> = mission.steps.iter().map(|s| s.name.clone()).collect();
assert!(step_names.iter().any(|name| name.contains("Namespace")));
assert!(step_names.iter().any(|name| name.contains("ConfigMap")));
assert!(step_names.iter().any(|name| name.contains("Deployment")));
}
#[test]
fn test_resource_ordering() {
let resources = vec![
KubernetesResource {
api_version: "apps/v1".to_string(),
kind: "Deployment".to_string(),
name: "app".to_string(),
namespace: None,
spec: HashMap::new(),
dependencies: vec![],
},
KubernetesResource {
api_version: "v1".to_string(),
kind: "ConfigMap".to_string(),
name: "config".to_string(),
namespace: None,
spec: HashMap::new(),
dependencies: vec![],
},
KubernetesResource {
api_version: "v1".to_string(),
kind: "Namespace".to_string(),
name: "test".to_string(),
namespace: None,
spec: HashMap::new(),
dependencies: vec![],
},
];
let ordered = KubernetesParser::order_resources_by_dependencies(&resources).unwrap();
assert_eq!(ordered[0].kind, "Namespace");
assert_eq!(ordered[1].kind, "ConfigMap");
assert_eq!(ordered[2].kind, "Deployment");
}
#[test]
fn test_step_type_mapping() {
assert!(matches!(KubernetesParser::map_resource_to_step_type("Deployment"), StepType::Http));
assert!(matches!(KubernetesParser::map_resource_to_step_type("ConfigMap"), StepType::CreateFile));
assert!(matches!(KubernetesParser::map_resource_to_step_type("Job"), StepType::Command));
assert!(matches!(KubernetesParser::map_resource_to_step_type("Service"), StepType::Http));
}
#[test]
fn test_step_characteristics() {
let (timeout, continue_on_error) = KubernetesParser::get_step_characteristics("Job");
assert_eq!(timeout, 1800); assert!(continue_on_error);
let (timeout, continue_on_error) = KubernetesParser::get_step_characteristics("Deployment");
assert_eq!(timeout, 600); assert!(!continue_on_error); }
#[tokio::test]
async fn test_empty_manifest() {
let result = KubernetesParser::parse_string("").await;
assert!(result.is_ok());
let mission = result.unwrap();
assert_eq!(mission.steps.len(), 0);
assert!(mission.description.unwrap().contains("0 resources"));
}
#[tokio::test]
async fn test_cronjob_parsing() {
let k8s_manifest = r#"
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup-job
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: backup:latest
command: ["/bin/bash", "-c", "backup-script.sh"]
restartPolicy: Never
"#;
let result = KubernetesParser::parse_string(k8s_manifest).await;
assert!(result.is_ok());
let mission = result.unwrap();
assert!(mission.steps.len() >= 1);
let step_names: Vec<String> = mission.steps.iter().map(|s| s.name.clone()).collect();
assert!(step_names.iter().any(|name| name.contains("CronJob") && name.contains("backup-job")));
}
}