use devops_models::models::ansible::AnsiblePlay;
use devops_models::models::docker_compose::DockerCompose;
use devops_models::models::github_actions::GitHubActions;
use devops_models::models::gitlab::GitLabCI;
use devops_models::models::helm::HelmValues;
use devops_models::models::k8s::*;
use devops_models::models::k8s_networking::{K8sIngress, K8sNetworkPolicy};
use devops_models::models::k8s_rbac::{K8sRole, K8sRoleBinding, K8sServiceAccount};
use devops_models::models::k8s_storage::K8sPVC;
use devops_models::models::k8s_workloads::{K8sCronJob, K8sDaemonSet, K8sHPA, K8sJob, K8sStatefulSet};
use devops_models::models::prometheus::{AlertmanagerConfig, PrometheusConfig};
use devops_models::models::validation::{
ConfigValidator, Diagnostic, ValidationResult, YamlType,
};
pub fn parse_yaml(content: &str) -> Result<serde_json::Value, String> {
let value: serde_json::Value =
serde_yaml::from_str(content).map_err(|e| format!("YAML parse error: {e}"))?;
if !value.is_object() && !value.is_array() {
return Err(format!(
"YAML must be a mapping or array, got: {}",
value_type_name(&value)
));
}
Ok(value)
}
pub fn detect_yaml_type(data: &serde_json::Value) -> YamlType {
if data.is_array() && AnsiblePlay::looks_like_playbook(data) {
return YamlType::Ansible;
}
let obj = match data.as_object() {
Some(o) => o,
None => return YamlType::Generic,
};
if obj.contains_key("apiVersion") && obj.contains_key("kind") {
let kind = obj.get("kind").and_then(|v| v.as_str()).unwrap_or("");
return match kind {
"Deployment" => YamlType::K8sDeployment,
"Service" => YamlType::K8sService,
"ConfigMap" => YamlType::K8sConfigMap,
"Secret" => YamlType::K8sSecret,
"Ingress" => YamlType::K8sIngress,
"HorizontalPodAutoscaler" => YamlType::K8sHPA,
"CronJob" => YamlType::K8sCronJob,
"Job" => YamlType::K8sJob,
"PersistentVolumeClaim" => YamlType::K8sPVC,
"NetworkPolicy" => YamlType::K8sNetworkPolicy,
"StatefulSet" => YamlType::K8sStatefulSet,
"DaemonSet" => YamlType::K8sDaemonSet,
"Role" => YamlType::K8sRole,
"ClusterRole" => YamlType::K8sClusterRole,
"RoleBinding" => YamlType::K8sRoleBinding,
"ClusterRoleBinding" => YamlType::K8sClusterRoleBinding,
"ServiceAccount" => YamlType::K8sServiceAccount,
_ => YamlType::K8sGeneric,
};
}
if obj.contains_key("services")
&& let Some(svcs) = obj.get("services").and_then(|v| v.as_object()) {
let looks_like_compose = svcs.values().any(|v| v.is_object());
if looks_like_compose {
return YamlType::DockerCompose;
}
}
if obj.contains_key("on") && obj.contains_key("jobs") {
return YamlType::GitHubActions;
}
if obj.contains_key("stages") || obj.values().any(|v| v.get("script").is_some()) {
return YamlType::GitLabCI;
}
if obj.contains_key("scrape_configs")
|| obj
.get("global")
.and_then(|g| g.get("scrape_interval"))
.is_some()
{
return YamlType::Prometheus;
}
if obj.contains_key("route") && obj.contains_key("receivers") {
return YamlType::Alertmanager;
}
if HelmValues::looks_like_helm(data) {
return YamlType::HelmValues;
}
if obj.contains_key("openapi") || obj.contains_key("swagger") {
return YamlType::OpenAPI;
}
YamlType::Generic
}
pub fn validate_k8s_manifest(content: &str) -> ValidationResult {
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: None,
}
}
};
let kind = data
.get("kind")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let extra_warnings = collect_k8s_warnings(&data);
match kind.as_str() {
"Deployment" => match serde_json::from_value::<K8sDeployment>(data) {
Ok(dep) => {
let mut warnings = extra_warnings;
warnings.extend(dep.validate());
ValidationResult {
valid: true,
errors: vec![],
diagnostics: vec![],
warnings,
hints: vec![],
yaml_type: Some(YamlType::K8sDeployment),
}
}
Err(e) => ValidationResult {
valid: false,
errors: format_serde_errors(&e),
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::K8sDeployment),
},
},
"Service" => match serde_json::from_value::<K8sService>(data) {
Ok(svc) => {
let mut warnings = extra_warnings;
warnings.extend(validate_service_semantics(&svc));
ValidationResult {
valid: true,
errors: vec![],
diagnostics: vec![],
warnings,
hints: vec![],
yaml_type: Some(YamlType::K8sService),
}
}
Err(e) => ValidationResult {
valid: false,
errors: format_serde_errors(&e),
diagnostics: vec![],
warnings: vec![],
hints: vec![],
yaml_type: Some(YamlType::K8sService),
},
},
"ConfigMap" => match serde_json::from_value::<K8sConfigMap>(data) {
Ok(cm) => {
let mut warnings = extra_warnings;
if cm.data.is_empty() && cm.binary_data.is_none() {
warnings.push("ConfigMap has no data and no binaryData".to_string());
}
ValidationResult {
valid: true,
errors: vec![],
diagnostics: vec![],
warnings,
hints: vec![],
yaml_type: Some(YamlType::K8sConfigMap),
}
}
Err(e) => ValidationResult {
valid: false,
errors: format_serde_errors(&e),
diagnostics: vec![],
warnings: vec![],
hints: vec![],
yaml_type: Some(YamlType::K8sConfigMap),
},
},
"Secret" => match serde_json::from_value::<K8sSecret>(data) {
Ok(sec) => {
let mut warnings = extra_warnings;
warnings.extend(validate_secret_semantics(&sec));
ValidationResult {
valid: true,
errors: vec![],
diagnostics: vec![],
warnings,
hints: vec![],
yaml_type: Some(YamlType::K8sSecret),
}
}
Err(e) => ValidationResult {
valid: false,
errors: format_serde_errors(&e),
diagnostics: vec![],
warnings: vec![],
hints: vec![],
yaml_type: Some(YamlType::K8sSecret),
},
},
"Ingress" => validate_k8s_with_trait::<K8sIngress>(data, YamlType::K8sIngress),
"HorizontalPodAutoscaler" => validate_k8s_with_trait::<K8sHPA>(data, YamlType::K8sHPA),
"CronJob" => validate_k8s_with_trait::<K8sCronJob>(data, YamlType::K8sCronJob),
"Job" => validate_k8s_with_trait::<K8sJob>(data, YamlType::K8sJob),
"PersistentVolumeClaim" => validate_k8s_with_trait::<K8sPVC>(data, YamlType::K8sPVC),
"NetworkPolicy" => {
validate_k8s_with_trait::<K8sNetworkPolicy>(data, YamlType::K8sNetworkPolicy)
}
"StatefulSet" => {
validate_k8s_with_trait::<K8sStatefulSet>(data, YamlType::K8sStatefulSet)
}
"DaemonSet" => validate_k8s_with_trait::<K8sDaemonSet>(data, YamlType::K8sDaemonSet),
"Role" | "ClusterRole" => validate_k8s_with_trait::<K8sRole>(data, YamlType::K8sRole),
"RoleBinding" | "ClusterRoleBinding" => {
validate_k8s_with_trait::<K8sRoleBinding>(data, YamlType::K8sRoleBinding)
}
"ServiceAccount" => {
validate_k8s_with_trait::<K8sServiceAccount>(data, YamlType::K8sServiceAccount)
}
_ => validate_k8s_generic(data, &kind),
}
}
fn validate_k8s_with_trait<T>(data: serde_json::Value, yaml_type: YamlType) -> ValidationResult
where
T: serde::de::DeserializeOwned + ConfigValidator,
{
match serde_json::from_value::<T>(data) {
Ok(resource) => resource.validate(),
Err(e) => ValidationResult {
valid: false,
errors: format_serde_errors(&e),
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(yaml_type),
},
}
}
fn validate_k8s_generic(data: serde_json::Value, kind: &str) -> ValidationResult {
let mut warnings = vec![format!(
"Kind '{}' has no specific validator — only basic structure checked",
kind
)];
let obj = data.as_object();
if let Some(o) = obj {
if !o.contains_key("metadata") {
warnings
.push(format!("Kind '{}' is missing 'metadata' — unusual for a K8s resource", kind));
}
if !o.contains_key("spec") && !o.contains_key("data") && !o.contains_key("rules") {
warnings.push(format!("Kind '{}' has no 'spec', 'data', or 'rules' field", kind));
}
}
ValidationResult {
valid: true,
errors: vec![],
diagnostics: vec![],
warnings,
hints: vec![],
yaml_type: Some(YamlType::K8sGeneric),
}
}
pub fn validate_gitlab_ci(content: &str) -> ValidationResult {
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::GitLabCI),
}
}
};
match GitLabCI::from_value(&data) {
Ok(ci) => {
let warnings = ci.validate();
ValidationResult {
valid: true,
errors: vec![],
warnings,
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::GitLabCI),
}
}
Err(e) => ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::GitLabCI),
},
}
}
pub fn validate_docker_compose(content: &str) -> ValidationResult {
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::DockerCompose),
}
}
};
match DockerCompose::from_value(data) {
Ok(compose) => compose.validate(),
Err(e) => ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::DockerCompose),
},
}
}
pub fn validate_github_actions(content: &str) -> ValidationResult {
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::GitHubActions),
}
}
};
match GitHubActions::from_value(&data) {
Ok(actions) => actions.validate(),
Err(e) => ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::GitHubActions),
},
}
}
pub fn validate_prometheus(content: &str) -> ValidationResult {
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::Prometheus),
}
}
};
match PrometheusConfig::from_value(data) {
Ok(config) => config.validate(),
Err(e) => ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::Prometheus),
},
}
}
pub fn validate_alertmanager(content: &str) -> ValidationResult {
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::Alertmanager),
}
}
};
match AlertmanagerConfig::from_value(data) {
Ok(config) => config.validate(),
Err(e) => ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::Alertmanager),
},
}
}
pub fn validate_helm_values(content: &str) -> ValidationResult {
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::HelmValues),
}
}
};
match HelmValues::from_value(&data) {
Ok(values) => values.validate(),
Err(e) => ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::HelmValues),
},
}
}
pub fn validate_ansible(content: &str) -> ValidationResult {
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::Ansible),
}
}
};
let playbook: Vec<AnsiblePlay> = match serde_json::from_value(data) {
Ok(p) => p,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![format!("Failed to parse Ansible playbook: {e}")],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::Ansible),
}
}
};
playbook.validate()
}
pub fn validate_auto(content: &str) -> ValidationResult {
let documents = split_yaml_documents(content);
if documents.len() > 1 {
return validate_multi_document(&documents);
}
let data = match parse_yaml(content) {
Ok(d) => d,
Err(e) => {
return ValidationResult {
valid: false,
errors: vec![e],
warnings: vec![],
diagnostics: vec![],
hints: vec![],
yaml_type: None,
}
}
};
let yaml_type = detect_yaml_type(&data);
match yaml_type {
YamlType::K8sDeployment
| YamlType::K8sService
| YamlType::K8sConfigMap
| YamlType::K8sSecret
| YamlType::K8sIngress
| YamlType::K8sHPA
| YamlType::K8sCronJob
| YamlType::K8sJob
| YamlType::K8sPVC
| YamlType::K8sNetworkPolicy
| YamlType::K8sStatefulSet
| YamlType::K8sDaemonSet
| YamlType::K8sRole
| YamlType::K8sClusterRole
| YamlType::K8sRoleBinding
| YamlType::K8sClusterRoleBinding
| YamlType::K8sServiceAccount
| YamlType::K8sGeneric => validate_k8s_manifest(content),
YamlType::GitLabCI => validate_gitlab_ci(content),
YamlType::DockerCompose => validate_docker_compose(content),
YamlType::GitHubActions => validate_github_actions(content),
YamlType::Prometheus => validate_prometheus(content),
YamlType::Alertmanager => validate_alertmanager(content),
YamlType::HelmValues => validate_helm_values(content),
YamlType::Ansible => validate_ansible(content),
YamlType::OpenAPI => ValidationResult {
valid: true,
errors: vec![],
warnings: vec!["OpenAPI validation not yet implemented — parsed OK".to_string()],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::OpenAPI),
},
YamlType::Generic => ValidationResult {
valid: true,
errors: vec![],
warnings: vec!["Generic YAML — no schema validation applied".to_string()],
diagnostics: vec![],
hints: vec![],
yaml_type: Some(YamlType::Generic),
},
}
}
fn split_yaml_documents(content: &str) -> Vec<String> {
let mut documents = Vec::new();
let mut current = String::new();
for line in content.lines() {
if line.trim() == "---" && !current.trim().is_empty() {
documents.push(current.clone());
current.clear();
} else if line.trim() != "---" {
current.push_str(line);
current.push('\n');
}
}
if !current.trim().is_empty() {
documents.push(current);
}
documents
}
fn validate_multi_document(documents: &[String]) -> ValidationResult {
let mut all_errors = Vec::new();
let mut all_warnings = Vec::new();
let mut all_diagnostics = Vec::new();
let mut all_valid = true;
let mut types_seen = Vec::new();
for (i, doc) in documents.iter().enumerate() {
let prefix = format!("[doc {}] ", i + 1);
let result = validate_auto(doc.trim());
if !result.valid {
all_valid = false;
}
for e in &result.errors {
all_errors.push(format!("{}{}", prefix, e));
}
for w in &result.warnings {
all_warnings.push(format!("{}{}", prefix, w));
}
for d in &result.diagnostics {
all_diagnostics.push(Diagnostic {
severity: d.severity.clone(),
message: format!("{}{}", prefix, d.message),
path: d.path.as_ref().map(|p| format!("{}{}", prefix, p)),
});
}
if let Some(t) = &result.yaml_type {
types_seen.push(format!("{}", t));
}
}
let type_summary = if types_seen.is_empty() {
"Generic".to_string()
} else {
types_seen.join(", ")
};
all_warnings.insert(
0,
format!(
"Multi-document YAML: {} documents detected ({})",
documents.len(),
type_summary
),
);
ValidationResult {
valid: all_valid,
errors: all_errors,
warnings: all_warnings,
diagnostics: all_diagnostics,
hints: vec![],
yaml_type: Some(YamlType::Generic), }
}
fn collect_k8s_warnings(data: &serde_json::Value) -> Vec<String> {
let mut warnings = Vec::new();
let kind = data.get("kind").and_then(|v| v.as_str()).unwrap_or("");
if kind == "Deployment" {
let spec = data.get("spec");
let replicas = spec
.and_then(|s| s.get("replicas"))
.and_then(|r| r.as_u64())
.unwrap_or(1);
if replicas == 1 {
let ns = data
.get("metadata")
.and_then(|m| m.get("namespace"))
.and_then(|n| n.as_str())
.unwrap_or("default");
warnings.push(format!(
"replicas=1 in namespace '{}' — consider >=2 for high availability",
ns
));
}
if let Some(containers) = spec
.and_then(|s| s.get("template"))
.and_then(|t| t.get("spec"))
.and_then(|s| s.get("containers"))
.and_then(|c| c.as_array())
{
for c in containers {
let name = c.get("name").and_then(|n| n.as_str()).unwrap_or("?");
let has_limits = c
.get("resources")
.and_then(|r| r.get("limits"))
.is_some();
if !has_limits {
warnings.push(format!(
"Container '{}' has no resource limits — may cause OOM kills",
name
));
}
let has_liveness = c.get("livenessProbe").is_some();
let has_readiness = c.get("readinessProbe").is_some();
if !has_liveness {
warnings.push(format!(
"Container '{}' has no livenessProbe — Kubernetes won't detect hangs",
name
));
}
if !has_readiness {
warnings.push(format!(
"Container '{}' has no readinessProbe — traffic may be sent to unready pods",
name
));
}
if let Some(image) = c.get("image").and_then(|i| i.as_str())
&& (image.ends_with(":latest") || !image.contains(':')) {
warnings.push(format!(
"Container '{}': image '{}' uses ':latest' or no tag — pin a specific version for reproducibility",
name, image
));
}
if let Some(policy) = c.get("imagePullPolicy").and_then(|p| p.as_str())
&& policy == "Never" {
warnings.push(format!(
"Container '{}': imagePullPolicy=Never — image must be pre-loaded on every node",
name
));
}
}
}
}
warnings
}
fn validate_service_semantics(svc: &K8sService) -> Vec<String> {
let mut warnings = Vec::new();
if let Some(svc_type) = &svc.spec.service_type {
if svc_type == "LoadBalancer" {
warnings.push("type=LoadBalancer creates a cloud load balancer — ensure this is intentional (cost implications)".to_string());
}
if svc_type == "NodePort" {
for port in &svc.spec.ports {
if port.port > 32767 {
warnings.push(format!(
"Port {} exceeds default NodePort range (30000-32767)",
port.port
));
}
}
}
}
if svc.spec.selector.is_empty() {
warnings.push("Service has an empty selector — will match no pods".to_string());
}
warnings
}
fn validate_secret_semantics(sec: &K8sSecret) -> Vec<String> {
let mut warnings = Vec::new();
if sec.data.is_empty() && sec.string_data.is_none() {
warnings.push("Secret has no data and no stringData".to_string());
}
for (key, val) in &sec.data {
if base64_decode_check(val).is_err() {
warnings.push(format!(
"Secret key '{}': value does not appear to be valid base64",
key
));
}
}
warnings
}
fn base64_decode_check(s: &str) -> Result<(), ()> {
let stripped = s.trim();
if stripped.is_empty() {
return Ok(());
}
let base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r ";
if stripped.chars().all(|c| base64_chars.contains(c)) {
Ok(())
} else {
Err(())
}
}
fn value_type_name(v: &serde_json::Value) -> &'static str {
match v {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}
fn format_serde_errors(e: &serde_json::Error) -> Vec<String> {
vec![e.to_string()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_k8s_deployment() {
let yaml = r#"apiVersion: apps/v1
kind: Deployment
metadata:
name: test
spec:
replicas: 1
selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
containers:
- name: app
image: nginx:1.25"#;
let data = parse_yaml(yaml).unwrap();
assert_eq!(detect_yaml_type(&data), YamlType::K8sDeployment);
}
#[test]
fn detect_k8s_ingress() {
let yaml = r#"apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test
spec:
rules: []"#;
let data = parse_yaml(yaml).unwrap();
assert_eq!(detect_yaml_type(&data), YamlType::K8sIngress);
}
#[test]
fn detect_k8s_generic_unknown_kind() {
let yaml = r#"apiVersion: custom.io/v1
kind: MyCustomResource
metadata:
name: test
spec:
foo: bar"#;
let data = parse_yaml(yaml).unwrap();
assert_eq!(detect_yaml_type(&data), YamlType::K8sGeneric);
}
#[test]
fn detect_docker_compose() {
let yaml = r#"services:
web:
image: nginx
ports:
- "8080:80"
db:
image: postgres:15"#;
let data = parse_yaml(yaml).unwrap();
assert_eq!(detect_yaml_type(&data), YamlType::DockerCompose);
}
#[test]
fn detect_github_actions() {
let yaml = r#"name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4"#;
let data = parse_yaml(yaml).unwrap();
assert_eq!(detect_yaml_type(&data), YamlType::GitHubActions);
}
#[test]
fn detect_gitlab_ci() {
let yaml = r#"stages:
- build
- test
build_job:
stage: build
script:
- echo hello"#;
let data = parse_yaml(yaml).unwrap();
assert_eq!(detect_yaml_type(&data), YamlType::GitLabCI);
}
#[test]
fn detect_prometheus() {
let yaml = r#"global:
scrape_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ['localhost:9090']"#;
let data = parse_yaml(yaml).unwrap();
assert_eq!(detect_yaml_type(&data), YamlType::Prometheus);
}
#[test]
fn detect_alertmanager() {
let yaml = r#"route:
receiver: default
receivers:
- name: default"#;
let data = parse_yaml(yaml).unwrap();
assert_eq!(detect_yaml_type(&data), YamlType::Alertmanager);
}
#[test]
fn validate_valid_deployment() {
let yaml = r#"apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: app
image: myapp:v1.2.3
ports:
- containerPort: 8080
resources:
limits:
memory: "128Mi"
cpu: "500m"
requests:
memory: "64Mi"
cpu: "250m"
livenessProbe:
httpGet:
path: /healthz
port: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080"#;
let result = validate_auto(yaml);
assert!(result.valid, "Expected valid, got errors: {:?}", result.errors);
}
#[test]
fn validate_hpa_min_gt_max() {
let yaml = r#"apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: test
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: test
minReplicas: 10
maxReplicas: 5"#;
let result = validate_auto(yaml);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("minReplicas")));
}
#[test]
fn validate_cronjob_invalid_schedule() {
let yaml = r#"apiVersion: batch/v1
kind: CronJob
metadata:
name: test
spec:
schedule: "not a cron"
jobTemplate:
spec:
template:
metadata:
labels:
app: test
spec:
containers:
- name: job
image: busybox
restartPolicy: OnFailure"#;
let result = validate_auto(yaml);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("cron schedule")));
}
#[test]
fn validate_docker_compose_no_image() {
let yaml = r#"services:
web:
ports:
- "8080:80""#;
let result = validate_auto(yaml);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("image") || e.contains("build")));
}
#[test]
fn validate_github_actions_no_on() {
let yaml = r#"name: CI
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hello"#;
let result = validate_github_actions(yaml);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("'on'")));
}
#[test]
fn validate_prometheus_duplicate_jobs() {
let yaml = r#"scrape_configs:
- job_name: myapp
static_configs:
- targets: ['localhost:9090']
- job_name: myapp
static_configs:
- targets: ['localhost:8080']"#;
let result = validate_prometheus(yaml);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("Duplicate job_name")));
}
#[test]
fn validate_alertmanager_missing_receiver() {
let yaml = r#"route:
receiver: missing
receivers:
- name: default"#;
let result = validate_alertmanager(yaml);
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.contains("not defined")));
}
#[test]
fn validate_k8s_generic_unknown_kind() {
let yaml = r#"apiVersion: custom.io/v1
kind: MyWidget
metadata:
name: test
spec:
replicas: 1"#;
let result = validate_auto(yaml);
assert!(result.valid);
assert!(result.warnings.iter().any(|w| w.contains("no specific validator")));
}
#[test]
fn validate_multi_document_yaml() {
let yaml = r#"apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: value
---
apiVersion: v1
kind: Service
metadata:
name: test
spec:
selector:
app: test
ports:
- port: 80"#;
let result = validate_auto(yaml);
assert!(result.valid);
assert!(result.warnings.iter().any(|w| w.contains("Multi-document")));
}
#[test]
fn split_documents() {
let yaml = "a: 1\n---\nb: 2\n---\nc: 3";
let docs = split_yaml_documents(yaml);
assert_eq!(docs.len(), 3);
}
#[test]
fn validate_secret_invalid_base64() {
let yaml = r#"apiVersion: v1
kind: Secret
metadata:
name: test
data:
password: "not-valid-base64!@#""#;
let result = validate_auto(yaml);
assert!(result.valid); assert!(result.warnings.iter().any(|w| w.contains("base64")));
}
}