use arcbox_constants::paths::{ArcboxProfile, HostLayout};
use arcbox_constants::ports::DOCKER_API_VSOCK_PORT;
use figment::{
Figment,
providers::{Env, Format, Serialized, Toml},
};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
#[serde(skip)]
pub profile: ArcboxProfile,
pub data_dir: PathBuf,
pub vm: VmDefaults,
pub machine: MachineDefaults,
pub network: NetworkConfig,
pub docker: DockerConfig,
pub container: ContainerRuntimeConfig,
pub logging: LoggingConfig,
pub storage: StorageConfig,
}
impl Default for Config {
fn default() -> Self {
Self::for_profile(ArcboxProfile::Production)
}
}
impl Config {
#[must_use]
pub fn for_profile(profile: ArcboxProfile) -> Self {
let layout = HostLayout::for_profile(profile);
Self {
profile,
data_dir: layout.data_dir,
vm: VmDefaults::default(),
machine: MachineDefaults::default(),
network: NetworkConfig::default(),
docker: DockerConfig::for_profile(profile),
container: ContainerRuntimeConfig::default(),
logging: LoggingConfig::default(),
storage: StorageConfig::default(),
}
}
pub fn load() -> Result<Self, Box<figment::Error>> {
Self::load_for_profile(ArcboxProfile::from_env_or_default())
}
pub fn load_for_profile(profile: ArcboxProfile) -> Result<Self, Box<figment::Error>> {
let mut config: Self = Figment::new()
.merge(Serialized::defaults(Self::for_profile(profile)))
.merge(Toml::file(system_config_path()))
.merge(Toml::file(user_config_path()))
.merge(Env::prefixed("ARCBOX_").split("_"))
.extract()
.map_err(Box::new)?;
config.profile = profile;
Ok(config)
}
pub fn load_from(path: impl AsRef<std::path::Path>) -> Result<Self, Box<figment::Error>> {
Figment::new()
.merge(Serialized::defaults(Self::default()))
.merge(Toml::file(path))
.merge(Env::prefixed("ARCBOX_").split("_"))
.extract()
.map_err(Box::new)
}
#[must_use]
pub fn data_subdir(&self) -> PathBuf {
self.data_dir.join(arcbox_constants::paths::host::DATA)
}
#[must_use]
pub fn images_dir(&self) -> PathBuf {
self.data_subdir().join("images")
}
#[must_use]
pub fn containers_dir(&self) -> PathBuf {
self.data_subdir().join("containers")
}
#[must_use]
pub fn machines_dir(&self) -> PathBuf {
self.data_subdir().join("machines")
}
#[must_use]
pub fn volumes_dir(&self) -> PathBuf {
self.data_subdir().join("volumes")
}
#[must_use]
pub fn run_dir(&self) -> PathBuf {
self.data_dir.join(arcbox_constants::paths::host::RUN)
}
#[must_use]
pub fn log_dir(&self) -> PathBuf {
self.data_dir.join(arcbox_constants::paths::host::LOG)
}
#[must_use]
pub fn docker_img_path(&self) -> PathBuf {
self.data_subdir().join("docker.img")
}
#[must_use]
pub fn docker_meta_img_path(&self) -> PathBuf {
self.data_subdir().join("docker-meta.img")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VmDefaults {
pub cpus: u32,
pub memory_mb: u64,
pub kernel_path: Option<PathBuf>,
pub backend: arcbox_vmm::VmBackend,
pub autostart: bool,
}
impl VmDefaults {
#[must_use]
pub fn effective_cpus(&self) -> u32 {
if self.cpus == 0 {
arcbox_hypervisor::default_vm_cpu_count()
} else {
self.cpus
}
}
}
impl Default for VmDefaults {
fn default() -> Self {
Self {
cpus: arcbox_hypervisor::default_vm_cpu_count(),
memory_mb: arcbox_hypervisor::default_vm_memory_size() / (1024 * 1024),
kernel_path: None,
backend: arcbox_vmm::VmBackend::default(),
autostart: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct MachineDefaults {
pub disk_gb: u64,
pub default_distro: String,
pub default_version: Option<String>,
pub auto_mount_home: bool,
}
impl Default for MachineDefaults {
fn default() -> Self {
Self {
disk_gb: 50,
default_distro: "ubuntu".to_string(),
default_version: None,
auto_mount_home: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NetworkConfig {
pub subnet: String,
pub gateway: Option<String>,
pub dns: Vec<String>,
pub ipv6: bool,
pub mtu: u16,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
subnet: "10.0.2.0/24".to_string(),
gateway: None,
dns: vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()],
ipv6: false,
mtu: 1500,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DockerConfig {
pub socket_path: PathBuf,
pub enabled: bool,
}
impl Default for DockerConfig {
fn default() -> Self {
Self::for_profile(ArcboxProfile::Production)
}
}
impl DockerConfig {
#[must_use]
pub fn for_profile(profile: ArcboxProfile) -> Self {
Self {
socket_path: HostLayout::for_profile(profile).docker_socket,
enabled: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ContainerRuntimeConfig {
pub guest_docker_vsock_port: u32,
pub startup_timeout_ms: u64,
}
impl Default for ContainerRuntimeConfig {
fn default() -> Self {
Self {
guest_docker_vsock_port: DOCKER_API_VSOCK_PORT,
startup_timeout_ms: 150_000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct LoggingConfig {
pub level: String,
pub file: Option<PathBuf>,
pub format: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
file: None,
format: "text".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct StorageConfig {
pub driver: String,
pub image_backend: String,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
driver: "overlay2".to_string(),
image_backend: "oci".to_string(),
}
}
}
fn user_config_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("~/.config"))
.join("arcbox")
.join("config.toml")
}
fn system_config_path() -> PathBuf {
PathBuf::from("/etc/arcbox/config.toml")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.profile, ArcboxProfile::Production);
assert_eq!(config.vm.cpus, arcbox_hypervisor::default_vm_cpu_count());
let expected_mb = arcbox_hypervisor::default_vm_memory_size() / (1024 * 1024);
assert_eq!(config.vm.memory_mb, expected_mb);
assert!(config.vm.memory_mb >= 512);
assert!(config.vm.memory_mb <= 16384);
assert_eq!(config.machine.disk_gb, 50);
assert!(config.docker.enabled);
assert_eq!(
config.container.guest_docker_vsock_port,
DOCKER_API_VSOCK_PORT
);
}
#[test]
fn development_profile_is_preserved_for_runtime_policy() {
assert_eq!(
Config::for_profile(ArcboxProfile::Development).profile,
ArcboxProfile::Development
);
}
#[test]
fn test_effective_cpus_zero_resolves_to_default() {
let vm = VmDefaults {
cpus: 0,
..VmDefaults::default()
};
assert_eq!(
vm.effective_cpus(),
arcbox_hypervisor::default_vm_cpu_count()
);
}
#[test]
fn test_effective_cpus_explicit_passes_through() {
let vm = VmDefaults {
cpus: 3,
..VmDefaults::default()
};
assert_eq!(vm.effective_cpus(), 3);
}
#[test]
fn vm_backend_defaults_to_vz() {
assert_eq!(Config::default().vm.backend, arcbox_vmm::VmBackend::Vz);
}
#[test]
fn vm_backend_parses_from_toml() {
let config: Config = Figment::new()
.merge(Serialized::defaults(Config::default()))
.merge(Toml::string("[vm]\nbackend = \"hv\""))
.extract()
.expect("config with vm.backend");
assert_eq!(config.vm.backend, arcbox_vmm::VmBackend::Hv);
}
#[test]
#[allow(clippy::result_large_err, reason = "figment::Jail closure signature")]
fn vm_backend_parses_from_env() {
figment::Jail::expect_with(|jail| {
jail.set_env("ARCBOX_VM_BACKEND", "hv");
let config: Config = Figment::new()
.merge(Serialized::defaults(Config::default()))
.merge(Env::prefixed("ARCBOX_").split("_"))
.extract()?;
assert_eq!(config.vm.backend, arcbox_vmm::VmBackend::Hv);
Ok(())
});
}
#[test]
fn test_config_paths() {
let config = Config::default();
assert!(config.images_dir().ends_with("data/images"));
assert!(config.containers_dir().ends_with("data/containers"));
assert!(config.machines_dir().ends_with("data/machines"));
assert!(config.volumes_dir().ends_with("data/volumes"));
assert!(config.run_dir().ends_with("run"));
assert!(config.log_dir().ends_with("log"));
assert!(config.docker_img_path().ends_with("data/docker.img"));
}
}