// thiserror/miette derive macros generate code that triggers false-positive unused_assignments #![allow(unused_assignments)] pub mod profile; pub mod resolve; pub mod schema; use miette::Diagnostic; use thiserror::Error; #[derive(Debug, Error, Diagnostic)] pub enum ParseError { #[error("Failed to parse KDL spec: {detail}")] #[diagnostic( help("Check the KDL syntax in your spec file"), code(spec_parser::kdl_parse) )] KdlError { detail: String }, } impl From for ParseError { fn from(err: knuffel::Error) -> Self { ParseError::KdlError { detail: err.to_string(), } } } pub fn parse(kdl: &str) -> Result { knuffel::parse("image.kdl", kdl).map_err(ParseError::from) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_example() { let kdl = r#" metadata name="my-image" version="1.0.0" description="A test image" base "path/to/base.tar.gz" build-host "path/to/build-vm.qcow2" repositories { publisher name="test-pub" origin="http://pkg.test.com" } incorporation "pkg:/test/incorporation" packages { package "system/kernel" } packages if="desktop" { package "desktop/gnome" } customization { user "admin" } overlays { file source="local/file" destination="/remote/file" } target "vm" kind="qcow2" { disk-size "20G" bootloader "grub" } target "container" kind="oci" { entrypoint command="/bin/sh" environment { set "PATH" "/bin:/usr/bin" } } "#; let spec = parse(kdl).expect("Failed to parse KDL"); assert_eq!(spec.metadata.name, "my-image"); assert_eq!(spec.base, Some("path/to/base.tar.gz".to_string())); assert_eq!( spec.build_host, Some("path/to/build-vm.qcow2".to_string()) ); assert_eq!(spec.repositories.publishers.len(), 1); assert_eq!(spec.packages.len(), 2); assert_eq!(spec.targets.len(), 2); let vm_target = &spec.targets[0]; assert_eq!(vm_target.name, "vm"); assert_eq!(vm_target.kind, schema::TargetKind::Qcow2); assert_eq!(vm_target.disk_size, Some("20G".to_string())); assert_eq!(vm_target.bootloader, Some("grub".to_string())); let container_target = &spec.targets[1]; assert_eq!(container_target.name, "container"); assert_eq!(container_target.kind, schema::TargetKind::Oci); assert_eq!( container_target.entrypoint.as_ref().unwrap().command, "/bin/sh" ); } #[test] fn test_parse_variants_and_certificates() { let kdl = r#" metadata name="test" version="0.1.0" repositories { publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/" } variants { set name="opensolaris.zone" value="global" } certificates { ca publisher="omnios" certfile="omniosce-ca.cert.pem" } "#; let spec = parse(kdl).expect("Failed to parse KDL"); let variants = spec.variants.unwrap(); assert_eq!(variants.vars.len(), 1); assert_eq!(variants.vars[0].name, "opensolaris.zone"); assert_eq!(variants.vars[0].value, "global"); let certs = spec.certificates.unwrap(); assert_eq!(certs.ca.len(), 1); assert_eq!(certs.ca[0].publisher, "omnios"); } #[test] fn test_parse_ubuntu_spec() { let kdl = r#" metadata name="ubuntu-ci" version="0.1.0" distro "ubuntu-22.04" repositories { apt-mirror "http://archive.ubuntu.com/ubuntu" suite="jammy" components="main universe" } packages { package "build-essential" package "curl" } target "qcow2" kind="qcow2" { disk-size "8G" bootloader "grub" filesystem "ext4" push-to "ghcr.io/cloudnebulaproject/ubuntu-rust:latest" } "#; let spec = parse(kdl).expect("Failed to parse Ubuntu spec"); assert_eq!(spec.distro, Some("ubuntu-22.04".to_string())); assert_eq!(spec.repositories.apt_mirrors.len(), 1); let mirror = &spec.repositories.apt_mirrors[0]; assert_eq!(mirror.url, "http://archive.ubuntu.com/ubuntu"); assert_eq!(mirror.suite, "jammy"); assert_eq!(mirror.components, Some("main universe".to_string())); let target = &spec.targets[0]; assert_eq!(target.filesystem, Some("ext4".to_string())); assert_eq!( target.push_to, Some("ghcr.io/cloudnebulaproject/ubuntu-rust:latest".to_string()) ); } #[test] fn test_parse_omnios_spec_unchanged() { // Existing OmniOS specs should parse without errors (backward compat) let kdl = r#" metadata name="omnios-disk" version="0.0.1" repositories { publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/" } packages { package "system/kernel" } target "vm" kind="qcow2" { disk-size "2000M" bootloader "uefi" pool { property name="ashift" value="12" } } "#; let spec = parse(kdl).expect("Failed to parse OmniOS spec"); assert_eq!(spec.distro, None); assert!(spec.repositories.apt_mirrors.is_empty()); assert_eq!(spec.targets[0].filesystem, None); assert_eq!(spec.targets[0].push_to, None); // DistroFamily should default to OmniOS assert_eq!( schema::DistroFamily::from_distro_str(spec.distro.as_deref()), schema::DistroFamily::OmniOS ); } #[test] fn test_distro_family_detection() { assert_eq!( schema::DistroFamily::from_distro_str(None), schema::DistroFamily::OmniOS ); assert_eq!( schema::DistroFamily::from_distro_str(Some("omnios")), schema::DistroFamily::OmniOS ); assert_eq!( schema::DistroFamily::from_distro_str(Some("ubuntu-22.04")), schema::DistroFamily::Ubuntu ); assert_eq!( schema::DistroFamily::from_distro_str(Some("ubuntu-24.04")), schema::DistroFamily::Ubuntu ); } #[test] fn test_parse_pool_properties() { let kdl = r#" metadata name="test" version="0.1.0" repositories {} target "disk" kind="qcow2" { disk-size "2000M" bootloader "uefi" pool { property name="ashift" value="12" } } "#; let spec = parse(kdl).expect("Failed to parse KDL"); let target = &spec.targets[0]; let pool = target.pool.as_ref().unwrap(); assert_eq!(pool.properties.len(), 1); assert_eq!(pool.properties[0].name, "ashift"); assert_eq!(pool.properties[0].value, "12"); } #[test] fn test_parse_builder_node_full() { let kdl = r#" metadata name="test" version="0.1.0" repositories {} builder { image "oci://ghcr.io/custom/builder:v1" vcpus 4 memory 4096 disk 50 } "#; let spec = parse(kdl).expect("Failed to parse KDL"); let builder = spec.builder.as_ref().unwrap(); assert_eq!(builder.image.as_deref(), Some("oci://ghcr.io/custom/builder:v1")); assert_eq!(builder.vcpus, Some(4)); assert_eq!(builder.memory, Some(4096)); assert_eq!(builder.disk, Some(50)); } #[test] fn test_parse_builder_node_partial() { let kdl = r#" metadata name="test" version="0.1.0" repositories {} builder { vcpus 8 } "#; let spec = parse(kdl).expect("Failed to parse KDL"); let builder = spec.builder.as_ref().unwrap(); assert_eq!(builder.image, None); assert_eq!(builder.vcpus, Some(8)); assert_eq!(builder.memory, None); } #[test] fn test_parse_builder_node_empty() { let kdl = r#" metadata name="test" version="0.1.0" repositories {} builder { } "#; let spec = parse(kdl).expect("Failed to parse KDL"); let builder = spec.builder.as_ref().unwrap(); assert_eq!(builder.image, None); assert_eq!(builder.vcpus, None); assert_eq!(builder.memory, None); assert_eq!(builder.disk, None); } #[test] fn test_parse_no_builder_node() { let kdl = r#" metadata name="test" version="0.1.0" repositories {} "#; let spec = parse(kdl).expect("Failed to parse KDL"); assert!(spec.builder.is_none()); } }