devops-models 0.1.0

Typed serde models for DevOps configuration formats: Kubernetes, Docker Compose, GitLab CI, GitHub Actions, Prometheus, Alertmanager, Helm, Ansible, and OpenAPI.
Documentation
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Kubernetes metadata — equivalent of Python K8sMetadata with extra="forbid"
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sMetadata {
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub namespace: Option<String>,
    #[serde(default)]
    pub labels: HashMap<String, String>,
    #[serde(default)]
    pub annotations: HashMap<String, String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sResourceRequirements {
    #[serde(default)]
    pub limits: HashMap<String, String>,
    #[serde(default)]
    pub requests: HashMap<String, String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sContainerPort {
    #[serde(rename = "containerPort")]
    pub container_port: u16,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub protocol: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sEnvVar {
    pub name: String,
    #[serde(default)]
    pub value: Option<String>,
    #[serde(default, rename = "valueFrom")]
    pub value_from: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sContainer {
    pub name: String,
    pub image: String,
    #[serde(default)]
    pub ports: Vec<K8sContainerPort>,
    #[serde(default)]
    pub env: Vec<K8sEnvVar>,
    #[serde(default)]
    pub resources: Option<K8sResourceRequirements>,
    #[serde(default)]
    pub command: Vec<String>,
    #[serde(default)]
    pub args: Vec<String>,
    #[serde(default, rename = "imagePullPolicy")]
    pub image_pull_policy: Option<String>,
    #[serde(default, rename = "livenessProbe")]
    pub liveness_probe: Option<serde_json::Value>,
    #[serde(default, rename = "readinessProbe")]
    pub readiness_probe: Option<serde_json::Value>,
    #[serde(default, rename = "volumeMounts")]
    pub volume_mounts: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sPodSpec {
    pub containers: Vec<K8sContainer>,
    #[serde(default, rename = "initContainers")]
    pub init_containers: Vec<K8sContainer>,
    #[serde(default, rename = "restartPolicy")]
    pub restart_policy: Option<String>,
    #[serde(default, rename = "serviceAccountName")]
    pub service_account_name: Option<String>,
    #[serde(default)]
    pub volumes: Option<serde_json::Value>,
    #[serde(default, rename = "nodeSelector")]
    pub node_selector: Option<HashMap<String, String>>,
    #[serde(default)]
    pub tolerations: Option<serde_json::Value>,
    #[serde(default)]
    pub affinity: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sPodTemplate {
    pub metadata: K8sMetadata,
    pub spec: K8sPodSpec,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sDeploymentSpec {
    pub replicas: Option<u32>,
    pub selector: serde_json::Value,
    pub template: K8sPodTemplate,
    #[serde(default)]
    pub strategy: Option<serde_json::Value>,
}

/// Kubernetes Deployment — top-level resource
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sDeployment {
    #[serde(rename = "apiVersion")]
    pub api_version: String,
    pub kind: String,
    pub metadata: K8sMetadata,
    pub spec: K8sDeploymentSpec,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sServicePort {
    pub port: u16,
    #[serde(default, rename = "targetPort")]
    pub target_port: Option<serde_json::Value>,
    #[serde(default)]
    pub protocol: Option<String>,
    #[serde(default)]
    pub name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sServiceSpec {
    pub selector: HashMap<String, String>,
    pub ports: Vec<K8sServicePort>,
    #[serde(default, rename = "type")]
    pub service_type: Option<String>,
    #[serde(default, rename = "clusterIP")]
    pub cluster_ip: Option<String>,
}

/// Kubernetes Service
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sService {
    #[serde(rename = "apiVersion")]
    pub api_version: String,
    pub kind: String,
    pub metadata: K8sMetadata,
    pub spec: K8sServiceSpec,
}

/// Kubernetes ConfigMap
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sConfigMap {
    #[serde(rename = "apiVersion")]
    pub api_version: String,
    pub kind: String,
    pub metadata: K8sMetadata,
    #[serde(default)]
    pub data: HashMap<String, String>,
    #[serde(default, rename = "binaryData")]
    pub binary_data: Option<HashMap<String, String>>,
}

/// Kubernetes Secret
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct K8sSecret {
    #[serde(rename = "apiVersion")]
    pub api_version: String,
    pub kind: String,
    pub metadata: K8sMetadata,
    #[serde(default, rename = "type")]
    pub secret_type: Option<String>,
    #[serde(default)]
    pub data: HashMap<String, String>,
    #[serde(default, rename = "stringData")]
    pub string_data: Option<HashMap<String, String>>,
}

/// Validation result for K8s resources
impl K8sDeployment {
    pub fn validate(&self) -> Vec<String> {
        let mut warnings = Vec::new();

        if self.kind != "Deployment" {
            warnings.push(format!("Expected kind 'Deployment', got '{}'", self.kind));
        }

        if let Some(replicas) = self.spec.replicas {
            if replicas == 0 {
                warnings.push("Replicas is 0 — no pods will be created".to_string());
            }
            if replicas > 100 {
                warnings.push(format!("Replicas is {} — unusually high", replicas));
            }
        }

        for container in &self.spec.template.spec.containers {
            if container.resources.is_none() {
                warnings.push(format!(
                    "Container '{}' has no resource limits/requests",
                    container.name
                ));
            }
            for port in &container.ports {
                if port.container_port == 0 {
                    warnings.push(format!(
                        "Container '{}': invalid port {}",
                        container.name, port.container_port
                    ));
                }
            }
        }

        warnings
    }
}