1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
5#[serde(rename_all = "camelCase", deny_unknown_fields)]
6pub struct DeploymentConfig {
7 pub target_ip: String,
8 #[serde(default)]
9 pub ssh_proxy_jump: String,
10 #[serde(default)]
11 pub ssh_identity_public_key: String,
12 pub builder: String,
13 #[serde(default = "default_build_on")]
14 pub build_on: String,
15 #[serde(default)]
16 pub bootstrap_user: String,
17 #[serde(default)]
18 pub require_secrets: bool,
19 pub low_mem: String,
20 #[serde(default = "default_substitute_on_destination")]
21 pub substitute_on_destination: bool,
22 #[serde(default)]
23 pub tailscale_tag: String,
24 #[serde(default)]
25 pub acl_tags: Vec<String>,
26 #[serde(default)]
27 pub tailnet_enrolled: bool,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub binary_cache: Option<BinaryCacheConfig>,
30 #[serde(default = "default_local_eval")]
31 pub local_eval: bool,
32 pub vmid: String,
33 pub disk_size: String,
34 #[serde(default)]
35 pub nameservers: Vec<String>,
36 pub proxmox: ProxmoxConfig,
37 pub digitalocean: DigitalOceanConfig,
38 pub vmware: VmwareConfig,
39 #[serde(default)]
40 pub wsl: WslConfig,
41}
42
43fn default_build_on() -> String {
44 "auto".to_string()
45}
46
47fn default_substitute_on_destination() -> bool {
48 true
49}
50
51#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct BinaryCacheConfig {
54 pub url: String,
55 pub public_key: String,
56 #[serde(default, skip_serializing_if = "String::is_empty")]
57 pub ca_certificate: String,
58}
59
60fn default_local_eval() -> bool {
61 true
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn destination_substitution_preserves_the_legacy_enabled_default() {
70 assert!(default_substitute_on_destination());
71 }
72
73 #[test]
76 fn absent_cache_policy_parses_as_no_cache() {
77 let parsed: NxdMetadata =
78 serde_json::from_value(serde_json::json!({})).expect("empty cache policy parses");
79 assert!(parsed.binary_cache.is_none());
80 }
81}
82
83#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
84#[serde(rename_all = "camelCase", deny_unknown_fields)]
85pub struct ProxmoxBootstrapConfig {
86 #[serde(default = "default_bootstrap_interface")]
87 pub interface: String,
88 #[serde(default)]
89 pub static_ip: String,
90 #[serde(default)]
91 pub subnet: String,
92 #[serde(default)]
93 pub gateway: String,
94 #[serde(default)]
95 pub vlan: Option<u16>,
96}
97
98fn default_bootstrap_interface() -> String {
99 "net0".to_string()
100}
101
102#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
103#[serde(rename_all = "camelCase", deny_unknown_fields)]
104pub struct ProxmoxConfig {
105 #[serde(default)]
106 pub provider: String,
107 #[serde(default)]
108 pub node: String,
109 pub host: String,
110 pub bios: String,
111 pub disk_bus: String,
112 pub scsi_hw: String,
113 pub disk_storage: String,
114 #[serde(default)]
115 pub discovery_subnets: Vec<String>,
116 #[serde(default)]
117 pub net0: String,
118 #[serde(default)]
119 pub net1: String,
120 #[serde(default)]
121 pub bootstrap: ProxmoxBootstrapConfig,
122 #[serde(default)]
123 pub extra_networks: Vec<String>,
124 #[serde(default)]
125 pub pxe: bool,
126 pub cores: String,
127 pub memory: String,
128 pub iso: ProxmoxIsoConfig,
129 pub cloud_init: CloudInitConfig,
130}
131
132#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
133#[serde(rename_all = "camelCase", deny_unknown_fields)]
134pub struct ProxmoxIsoConfig {
135 #[serde(rename = "type")]
137 pub flavor: String,
138 pub storage: String,
139 pub custom_path: String,
141}
142
143#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
144#[serde(rename_all = "camelCase", deny_unknown_fields)]
145pub struct CloudInitConfig {
146 pub image: String,
147 pub user: String,
148 pub ipconfig0: String,
149 pub ipconfig1: String,
150}
151
152#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
153#[serde(rename_all = "camelCase", deny_unknown_fields)]
154pub struct DigitalOceanConfig {
155 pub region: String,
156 pub size: String,
157 pub image: String,
158}
159
160#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
161#[serde(rename_all = "camelCase", deny_unknown_fields)]
162pub struct VmwareConfig {
163 pub vmx_path: String,
164 #[serde(default)]
165 pub iso_directory: String,
166 #[serde(default)]
167 pub cores: u16,
168 #[serde(default, rename = "memoryMiB")]
169 pub memory_mib: u32,
170}
171
172#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
173#[serde(rename_all = "camelCase", deny_unknown_fields)]
174pub struct WslConfig {
175 #[serde(default)]
176 pub enable: bool,
177 #[serde(default)]
178 pub windows_host: String,
179 #[serde(default)]
180 pub windows_user: String,
181 #[serde(default = "default_wsl_distribution")]
182 pub distribution: String,
183 #[serde(default)]
184 pub install_root: String,
185 #[serde(default = "default_wsl_bootstrap_user")]
186 pub bootstrap_user: String,
187 #[serde(default)]
188 pub guest_host: String,
189 #[serde(default = "default_wsl_transport")]
190 pub transport: String,
191}
192
193impl Default for WslConfig {
194 fn default() -> Self {
195 Self {
196 enable: false,
197 windows_host: String::new(),
198 windows_user: String::new(),
199 distribution: default_wsl_distribution(),
200 install_root: String::new(),
201 bootstrap_user: default_wsl_bootstrap_user(),
202 guest_host: String::new(),
203 transport: default_wsl_transport(),
204 }
205 }
206}
207
208fn default_wsl_distribution() -> String {
209 "NixOS".to_string()
210}
211
212fn default_wsl_bootstrap_user() -> String {
213 "nixos".to_string()
214}
215
216fn default_wsl_transport() -> String {
217 "auto".to_string()
218}
219
220#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
221#[serde(rename_all = "camelCase", deny_unknown_fields)]
222pub struct CrossConfig {
223 #[serde(default)]
224 pub local_system: Option<serde_json::Value>,
225 #[serde(default)]
226 pub cross_system: Option<serde_json::Value>,
227}
228
229fn default_build_system() -> bool {
230 true
231}
232
233#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
236#[serde(rename_all = "camelCase", deny_unknown_fields)]
237pub struct NxdMetadata {
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub binary_cache: Option<BinaryCacheConfig>,
240 #[serde(default, skip_serializing_if = "String::is_empty")]
244 pub secrets_site: String,
245}
246
247#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
252#[serde(rename_all = "camelCase", deny_unknown_fields)]
253pub struct RuntimeSecretInput {
254 pub path: String,
256 pub activation_unit: String,
258}
259
260#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
261#[serde(rename_all = "camelCase", deny_unknown_fields)]
262pub struct HostPlaneAttachment {
263 #[serde(default)]
264 pub enable: bool,
265 #[serde(default)]
267 pub provider: String,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub runtime_secret: Option<RuntimeSecretInput>,
272 #[serde(default, skip_serializing_if = "is_null_or_empty_object")]
274 pub policy: serde_json::Value,
275}
276
277fn is_null_or_empty_object(value: &serde_json::Value) -> bool {
278 value.is_null() || value.as_object().is_some_and(|object| object.is_empty())
279}
280
281#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
282#[serde(rename_all = "camelCase", deny_unknown_fields)]
283pub struct FlakeMetadata {
284 pub deployment: DeploymentConfig,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub evaluation_source: Option<String>,
287 #[serde(default, skip_serializing_if = "Vec::is_empty")]
288 pub evaluation_source_inputs: Vec<String>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub evaluation_secret_source: Option<String>,
291 #[serde(default)]
292 pub nxd: NxdMetadata,
293 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
296 pub plane_attachments: BTreeMap<String, HostPlaneAttachment>,
297 pub system: String,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub system_output: Option<String>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub system_derivation: Option<String>,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub disko_output: Option<String>,
304 pub user: String,
305 pub has_disko: bool,
306 #[serde(default = "default_build_system")]
307 pub build_system: bool,
308 #[serde(default)]
309 pub wsl: bool,
310 #[serde(default)]
311 pub class: String,
312 #[serde(default)]
313 pub server: bool,
314 #[serde(default)]
315 pub home: bool,
316 #[serde(default)]
317 pub role: Option<String>,
318 #[serde(default)]
319 pub tags: Vec<String>,
320 #[serde(default)]
321 pub cross: Option<CrossConfig>,
322 #[serde(default)]
323 pub features: Vec<serde_json::Value>,
324 #[serde(default)]
325 pub os_features: Vec<serde_json::Value>,
326 #[serde(default)]
327 pub hm_features: Vec<serde_json::Value>,
328}