Skip to main content

nxd_core/domain/
deployment_metadata.rs

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	/// A host declaring no cache must parse as absent rather than fail, since
74	/// `nxd` carries `deny_unknown_fields`.
75	#[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	/// ISO type; only "qemu" is supported.
136	#[serde(rename = "type")]
137	pub flavor: String,
138	pub storage: String,
139	/// Full Proxmox storage path override, e.g. "storage:iso/my.iso". Empty = use type.
140	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/// Mirrors the host value behind `deployment.binaryCache`, which is the
234/// projection NXD reads. Consumers assert the two agree.
235#[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	/// Secrets-repository site holding this host's material. Consumers author
241	/// identity bindings at evaluation time, before any filesystem lookup, so a
242	/// host outside the repository's own site must declare it.
243	#[serde(default, skip_serializing_if = "String::is_empty")]
244	pub secrets_site: String,
245}
246
247/// Generic host attachment to an optional external provider plane (e.g. client
248/// Tailnet enrollment). Core uses `enable` and `provider` for selection and
249/// composition. `policy` is opaque provider schema. Control-plane services
250/// remain host OS config, not this attachment.
251#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
252#[serde(rename_all = "camelCase", deny_unknown_fields)]
253pub struct RuntimeSecretInput {
254	/// Operation-private target path. It must be below `/run/nxd/enrollment/`.
255	pub path: String,
256	/// Exact consumer-owned systemd unit that consumes the file.
257	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	/// Provider instance id (e.g. `provider/headscale`). Required when enable.
266	#[serde(default)]
267	pub provider: String,
268	/// Optional Nix-owned runtime delivery contract for ephemeral enrollment.
269	/// The plane provider never sees or executes this target-local policy.
270	#[serde(default, skip_serializing_if = "Option::is_none")]
271	pub runtime_secret: Option<RuntimeSecretInput>,
272	/// Provider-owned policy (tags, users, leaf names, etc.). Opaque to core.
273	#[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	/// Named host plane attachments (e.g. `tailnet`). Consumer maps site-specific
294	/// authoring (such as LAMT `headscaleEnrollment`) into this common shape.
295	#[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}