1use serde::Deserialize;
2use std::collections::HashMap;
3use toml::map::Map;
4use toml::Value;
5
6#[derive(Debug, Deserialize)]
7pub struct RootConfig {
8 pub dotm: DotmSettings,
9 #[serde(default)]
10 pub packages: HashMap<String, PackageConfig>,
11}
12
13#[derive(Debug, Deserialize)]
14pub struct DotmSettings {
15 pub target: String,
16 #[serde(default = "default_packages_dir")]
17 pub packages_dir: String,
18 #[serde(default)]
19 pub auto_prune: bool,
20}
21
22fn default_packages_dir() -> String {
23 "packages".to_string()
24}
25
26#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
27#[serde(rename_all = "lowercase")]
28pub enum DeployStrategy {
29 Stage,
30 Copy,
31}
32
33#[derive(Debug, Default, Deserialize)]
34pub struct PackageConfig {
35 pub description: Option<String>,
36 #[serde(default)]
37 pub depends: Vec<String>,
38 #[serde(default)]
39 pub suggests: Vec<String>,
40 pub target: Option<String>,
41 pub strategy: Option<DeployStrategy>,
42 #[serde(default)]
43 pub permissions: HashMap<String, String>,
44 #[serde(default)]
45 pub system: bool,
46 pub owner: Option<String>,
47 pub group: Option<String>,
48 #[serde(default)]
49 pub ownership: HashMap<String, String>,
50 #[serde(default)]
51 pub preserve: HashMap<String, Vec<String>>,
52 pub pre_deploy: Option<String>,
53 pub post_deploy: Option<String>,
54 pub pre_undeploy: Option<String>,
55 pub post_undeploy: Option<String>,
56}
57
58pub fn validate_system_packages(root: &RootConfig) -> Vec<String> {
59 let mut errors = Vec::new();
60 for (name, pkg) in &root.packages {
61 if pkg.system {
62 if pkg.target.is_none() {
63 errors.push(format!(
64 "system package '{name}' must specify a target directory"
65 ));
66 }
67 if pkg.strategy.is_none() {
68 errors.push(format!(
69 "system package '{name}' must specify a deployment strategy"
70 ));
71 }
72 }
73 for (path, value) in &pkg.ownership {
75 if value.split(':').count() != 2 {
76 errors.push(format!(
77 "package '{name}': invalid ownership format for '{path}': expected 'user:group', got '{value}'"
78 ));
79 }
80 }
81 for (path, value) in &pkg.permissions {
83 if u32::from_str_radix(value, 8).is_err() {
84 errors.push(format!(
85 "package '{name}': invalid permission for '{path}': '{value}' is not valid octal"
86 ));
87 }
88 }
89 for (path, preserve_fields) in &pkg.preserve {
91 for field in preserve_fields {
92 match field.as_str() {
93 "owner" | "group" => {
94 if pkg.ownership.contains_key(path) {
95 errors.push(format!(
96 "package '{name}': file '{path}' has both preserve {field} and ownership override"
97 ));
98 }
99 }
100 "mode" => {
101 if pkg.permissions.contains_key(path) {
102 errors.push(format!(
103 "package '{name}': file '{path}' has both preserve mode and permission override"
104 ));
105 }
106 }
107 other => {
108 errors.push(format!(
109 "package '{name}': file '{path}': unknown preserve field '{other}'"
110 ));
111 }
112 }
113 }
114 }
115 }
116 errors
117}
118
119#[derive(Debug, Deserialize)]
120pub struct HostConfig {
121 pub hostname: String,
122 pub roles: Vec<String>,
123 #[serde(default)]
124 pub vars: Map<String, Value>,
125}
126
127#[derive(Debug, Deserialize)]
128pub struct RoleConfig {
129 pub packages: Vec<String>,
130 #[serde(default)]
131 pub vars: Map<String, Value>,
132}