#[allow(unused_imports)]
use crate::core::{codegen, executor, migrate, parser, planner, resolver, secrets, state, types};
use std::collections::HashSet;
use std::path::Path;
pub(crate) fn cmd_validate_check_orphan_resources(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let orphans = find_orphan_resources(&config);
if json {
let items: Vec<String> = orphans.iter().map(|o| format!("\"{o}\"")).collect();
println!("{{\"orphan_resources\":[{}]}}", items.join(","));
} else if orphans.is_empty() {
println!("No orphan resources (all participate in dependency chains).");
} else {
println!(
"Orphan resources ({}, not depended on and have no deps):",
orphans.len()
);
for o in &orphans {
println!(" {o}");
}
}
Ok(())
}
fn find_orphan_resources(config: &types::ForjarConfig) -> Vec<String> {
let mut depended_on: HashSet<&str> = HashSet::new();
let mut has_deps: HashSet<&str> = HashSet::new();
for (name, resource) in &config.resources {
if !resource.depends_on.is_empty() {
has_deps.insert(name.as_str());
for dep in &resource.depends_on {
depended_on.insert(dep.as_str());
}
}
}
let mut orphans: Vec<String> = config
.resources
.keys()
.filter(|n| !has_deps.contains(n.as_str()) && !depended_on.contains(n.as_str()))
.cloned()
.collect();
orphans.sort();
orphans
}
pub(crate) fn cmd_validate_check_machine_arch(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let valid_archs = [
"x86_64", "aarch64", "arm64", "armv7", "riscv64", "ppc64le", "s390x",
];
let mut bad: Vec<(String, String)> = Vec::new();
for (name, machine) in &config.machines {
let arch = machine.arch.as_str();
if !valid_archs.contains(&arch) {
bad.push((name.clone(), arch.to_string()));
}
}
bad.sort();
if json {
let items: Vec<String> = bad
.iter()
.map(|(m, a)| format!("{{\"machine\":\"{m}\",\"arch\":\"{a}\"}}"))
.collect();
println!("{{\"invalid_architectures\":[{}]}}", items.join(","));
} else if bad.is_empty() {
println!("All machine architectures are valid.");
} else {
println!("Invalid architectures ({}):", bad.len());
for (m, a) in &bad {
println!(" {} — \"{}\" (expected: {})", m, a, valid_archs.join(", "));
}
}
Ok(())
}
pub(crate) fn cmd_validate_check_resource_health_conflicts(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let conflicts = find_health_conflicts(&config);
if json {
let items: Vec<String> = conflicts
.iter()
.map(|(r, reason)| format!("{{\"resource\":\"{r}\",\"conflict\":\"{reason}\"}}"))
.collect();
println!("{{\"health_conflicts\":[{}]}}", items.join(","));
} else if conflicts.is_empty() {
println!("No resource health conflicts detected.");
} else {
println!("Resource health conflicts ({}):", conflicts.len());
for (r, reason) in &conflicts {
println!(" {r} — {reason}");
}
}
Ok(())
}
fn find_health_conflicts(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut conflicts = Vec::new();
for (name, resource) in &config.resources {
let rtype = format!("{:?}", resource.resource_type);
let has_service_state = resource.state.as_deref() == Some("running")
|| resource.state.as_deref() == Some("stopped");
let is_service = rtype.contains("Service");
if has_service_state && !is_service {
conflicts.push((
name.clone(),
format!("{name} has service state but is type {rtype}"),
));
}
if is_service && resource.state.as_deref() == Some("absent") {
conflicts.push((
name.clone(),
"service with state=absent is contradictory".to_string(),
));
}
}
conflicts.sort();
conflicts
}
pub(crate) fn cmd_validate_check_resource_overlap(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let overlaps = find_resource_overlaps(&config);
if json {
let items: Vec<String> = overlaps
.iter()
.map(|(a, b, m)| {
format!("{{\"resource_a\":\"{a}\",\"resource_b\":\"{b}\",\"machine\":\"{m}\"}}")
})
.collect();
println!("{{\"resource_overlaps\":[{}]}}", items.join(","));
} else if overlaps.is_empty() {
println!("No overlapping resources detected.");
} else {
println!("Overlapping resources ({}):", overlaps.len());
for (a, b, m) in &overlaps {
println!(" {a} <-> {b} on {m}");
}
}
Ok(())
}
fn find_resource_overlaps(config: &types::ForjarConfig) -> Vec<(String, String, String)> {
let mut overlaps = Vec::new();
let names: Vec<&String> = config.resources.keys().collect();
for i in 0..names.len() {
for j in (i + 1)..names.len() {
let ra = &config.resources[names[i]];
let rb = &config.resources[names[j]];
let shared = shared_machines(ra, rb);
if shared.is_empty() {
continue;
}
if resources_conflict(ra, rb) {
for m in shared {
overlaps.push((names[i].clone(), names[j].clone(), m));
}
}
}
}
overlaps
}
fn shared_machines(ra: &types::Resource, rb: &types::Resource) -> Vec<String> {
let mb: Vec<&str> = rb.machine.iter().collect();
ra.machine
.iter()
.filter(|m| mb.contains(m))
.map(|s| s.to_owned())
.collect()
}
fn resources_conflict(ra: &types::Resource, rb: &types::Resource) -> bool {
let same_type =
std::mem::discriminant(&ra.resource_type) == std::mem::discriminant(&rb.resource_type);
let same_path = ra.path.is_some() && ra.path == rb.path;
let same_port = ra.port.is_some() && ra.port == rb.port;
let same_name = ra.name.is_some() && ra.name == rb.name;
let same_target = ra.target.is_some() && ra.target == rb.target;
same_type && (same_path || same_port || same_name || same_target)
}
pub(crate) fn cmd_validate_check_resource_tags(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let issues = find_tag_issues(&config);
if json {
let items: Vec<String> = issues
.iter()
.map(|(r, issue)| format!("{{\"resource\":\"{r}\",\"issue\":\"{issue}\"}}"))
.collect();
println!("{{\"tag_issues\":[{}]}}", items.join(","));
} else if issues.is_empty() {
println!("All resource tags follow conventions.");
} else {
println!("Tag convention issues ({}):", issues.len());
for (r, issue) in &issues {
println!(" {r} — {issue}");
}
}
Ok(())
}
fn find_tag_issues(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut issues = Vec::new();
for (name, resource) in &config.resources {
if resource.tags.is_empty() {
issues.push((name.clone(), "no tags assigned".to_string()));
continue;
}
for tag in &resource.tags {
if tag != &tag.to_lowercase() {
issues.push((name.clone(), format!("tag '{tag}' should be lowercase")));
}
if tag.contains(' ') {
issues.push((name.clone(), format!("tag '{tag}' contains spaces")));
}
}
}
issues.sort();
issues
}
pub(crate) fn cmd_validate_check_resource_state_consistency(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let issues = find_state_consistency_issues(&config);
if json {
let items: Vec<String> = issues
.iter()
.map(|(r, issue)| format!("{{\"resource\":\"{r}\",\"issue\":\"{issue}\"}}"))
.collect();
println!("{{\"state_consistency_issues\":[{}]}}", items.join(","));
} else if issues.is_empty() {
println!("All resource states are consistent with their types.");
} else {
println!("State consistency issues ({}):", issues.len());
for (r, issue) in &issues {
println!(" {r} — {issue}");
}
}
Ok(())
}
fn find_state_consistency_issues(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut issues = Vec::new();
for (name, resource) in &config.resources {
if let Some(ref state) = resource.state {
let rtype = format!("{:?}", resource.resource_type);
if !is_valid_state_for_type(state, &rtype) {
issues.push((
name.clone(),
format!("state '{state}' invalid for type {rtype}"),
));
}
}
}
issues.sort();
issues
}
fn is_valid_state_for_type(state: &str, rtype: &str) -> bool {
if rtype.contains("Package") {
["present", "absent", "latest"].contains(&state)
} else if rtype.contains("Service") {
["running", "stopped", "enabled", "disabled"].contains(&state)
} else if rtype.contains("File") || rtype.contains("Template") {
["present", "absent", "directory"].contains(&state)
} else {
true
}
}
pub(crate) fn cmd_validate_check_resource_dependencies_complete(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let missing = find_missing_deps(&config);
if json {
let items: Vec<String> = missing
.iter()
.map(|(r, dep)| format!("{{\"resource\":\"{r}\",\"missing_dep\":\"{dep}\"}}"))
.collect();
println!("{{\"missing_dependencies\":[{}]}}", items.join(","));
} else if missing.is_empty() {
println!("All dependency targets exist.");
} else {
println!("Missing dependency targets ({}):", missing.len());
for (r, dep) in &missing {
println!(" {r} depends on '{dep}' (not found)");
}
}
Ok(())
}
fn find_missing_deps(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut missing = Vec::new();
for (name, resource) in &config.resources {
for dep in &resource.depends_on {
if !config.resources.contains_key(dep) {
missing.push((name.clone(), dep.clone()));
}
}
}
missing.sort();
missing
}
pub(crate) fn cmd_validate_check_machine_connectivity(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let issues = check_machine_addrs(&config);
if json {
let items: Vec<String> = issues
.iter()
.map(|(m, issue)| format!("{{\"machine\":\"{m}\",\"issue\":\"{issue}\"}}"))
.collect();
println!("{{\"connectivity_issues\":[{}]}}", items.join(","));
} else if issues.is_empty() {
println!("All machine addresses look valid.");
} else {
println!("Machine connectivity issues ({}):", issues.len());
for (m, issue) in &issues {
println!(" {m} — {issue}");
}
}
Ok(())
}
fn check_machine_addrs(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut issues = Vec::new();
for (name, machine) in &config.machines {
let addr = machine.addr.as_str();
if addr.is_empty() {
issues.push((name.clone(), "empty address".to_string()));
} else if addr == "localhost" || addr == "127.0.0.1" || addr == "container" {
} else if !addr.contains('.') && !addr.contains(':') {
issues.push((name.clone(), format!("addr '{addr}' has no dots or colons")));
}
}
issues.sort();
issues
}