use url::Url;
use crate::error::{DeployError, Result};
use crate::types::ProjectConfig;
pub fn validate_project_config(config: &ProjectConfig, env: &str) -> Result<()> {
let mut errors: Vec<String> = Vec::new();
for svc in &config.services {
let Some(deploy) = &svc.deploy else {
continue;
};
let Some(env_cfg) = deploy.envs.get(env) else {
continue;
};
if let Some(oci) = &svc.oci {
if oci.image.trim().is_empty() {
errors.push(format!("service `{}` oci.image is empty", svc.name));
}
}
let provider: &str = deploy.provider.as_str();
if matches!(provider, "kubernetes" | "kubernetes-operator") {
let ns = env_cfg.namespace.clone().or_else(|| {
config
.kubernetes
.as_ref()
.and_then(|k| k.default_namespace.clone())
});
if ns.as_ref().map(|s| s.trim().is_empty()).unwrap_or(true) {
errors.push(format!(
"service `{}` kubernetes deploy requires namespace or kubernetes.default_namespace",
svc.name
));
}
}
for health in &env_cfg.health {
if Url::parse(health).is_err() {
errors.push(format!(
"service `{}` invalid health URL `{health}`",
svc.name
));
}
}
if svc.name.contains("athena") {
for path in [
env_cfg
.kubernetes
.as_ref()
.and_then(|k| k.crds_path.as_deref()),
env_cfg
.kubernetes
.as_ref()
.and_then(|k| k.install_path.as_deref()),
]
.into_iter()
.flatten()
{
if path.to_ascii_lowercase().contains("athenacluster") {
errors.push(format!(
"service `{}` must not reference AthenaCluster paths (`{path}`)",
svc.name
));
}
}
}
}
for (name, group) in &config.groups {
for svc in &group.services {
if !config.services.iter().any(|s| s.name == *svc) {
errors.push(format!(
"deploy.groups.{name} references unknown service `{svc}`"
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(DeployError::Validation(errors.join("; ")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;
use std::collections::HashMap;
use std::path::PathBuf;
#[test]
fn rejects_athenacluster_path() {
let mut envs: HashMap<String, ServiceDeployEnvView> = HashMap::new();
envs.insert(
"production".into(),
ServiceDeployEnvView {
namespace: Some("athena-system".into()),
replicas: None,
health: vec!["https://example.com/health".into()],
kubernetes: Some(ServiceK8sView {
manifests_base: None,
manifests_overlay: None,
workload: None,
service: None,
crds_path: Some("config/athenacluster".into()),
install_path: None,
selector: None,
}),
},
);
let config = ProjectConfig {
project_name: "x".into(),
version: "1".into(),
project_root: PathBuf::from("."),
services: vec![ServiceConfigView {
name: "athena-operator".into(),
root_directory: None,
version: None,
depends_on: vec![],
oci: None,
deploy: Some(ServiceDeployView {
provider: "kubernetes-operator".into(),
envs,
}),
}],
groups: HashMap::new(),
default_env: None,
kubernetes: None,
history_dir: PathBuf::from(".xbp/deployments"),
lock_file: PathBuf::from(".xbp/deploy-lock.json"),
git_sha: None,
};
let err = validate_project_config(&config, "production").unwrap_err();
assert!(err.to_string().contains("AthenaCluster"));
}
}