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, skip_serializing_if = "Option::is_none")]
23	pub binary_cache: Option<BinaryCacheConfig>,
24	#[serde(default = "default_local_eval")]
25	pub local_eval: bool,
26	pub vmid: String,
27	pub disk_size: String,
28	#[serde(default)]
29	pub nameservers: Vec<String>,
30	pub proxmox: ProxmoxConfig,
31	pub digitalocean: DigitalOceanConfig,
32	pub vmware: VmwareConfig,
33	#[serde(default)]
34	pub wsl: WslConfig,
35}
36
37fn default_build_on() -> String {
38	"auto".to_string()
39}
40
41fn default_substitute_on_destination() -> bool {
42	true
43}
44
45#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47pub struct BinaryCacheConfig {
48	pub url: String,
49	pub public_key: String,
50	#[serde(default, skip_serializing_if = "String::is_empty")]
51	pub ca_certificate: String,
52}
53
54fn default_local_eval() -> bool {
55	true
56}
57
58#[cfg(test)]
59mod tests {
60	use super::*;
61
62	#[test]
63	fn destination_substitution_preserves_the_legacy_enabled_default() {
64		assert!(default_substitute_on_destination());
65	}
66
67	/// A host declaring no cache must parse as absent rather than fail, since
68	/// `nxd` carries `deny_unknown_fields`.
69	#[test]
70	fn absent_cache_policy_parses_as_no_cache() {
71		let parsed: NxdMetadata =
72			serde_json::from_value(serde_json::json!({})).expect("empty cache policy parses");
73		assert!(parsed.binary_cache.is_none());
74	}
75}
76
77#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
78#[serde(rename_all = "camelCase", deny_unknown_fields)]
79pub struct ProxmoxBootstrapConfig {
80	#[serde(default = "default_bootstrap_interface")]
81	pub interface: String,
82	#[serde(default)]
83	pub static_ip: String,
84	#[serde(default)]
85	pub subnet: String,
86	#[serde(default)]
87	pub gateway: String,
88	#[serde(default)]
89	pub vlan: Option<u16>,
90}
91
92fn default_bootstrap_interface() -> String {
93	"net0".to_string()
94}
95
96#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
97#[serde(rename_all = "camelCase", deny_unknown_fields)]
98pub struct ProxmoxConfig {
99	#[serde(default)]
100	pub provider: String,
101	#[serde(default)]
102	pub node: String,
103	pub host: String,
104	pub bios: String,
105	pub disk_bus: String,
106	pub scsi_hw: String,
107	pub disk_storage: String,
108	#[serde(default)]
109	pub discovery_subnets: Vec<String>,
110	#[serde(default)]
111	pub net0: String,
112	#[serde(default)]
113	pub net1: String,
114	#[serde(default)]
115	pub bootstrap: ProxmoxBootstrapConfig,
116	#[serde(default)]
117	pub extra_networks: Vec<String>,
118	#[serde(default)]
119	pub pxe: bool,
120	pub cores: String,
121	pub memory: String,
122	pub iso: ProxmoxIsoConfig,
123	pub cloud_init: CloudInitConfig,
124}
125
126#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
127#[serde(rename_all = "camelCase", deny_unknown_fields)]
128pub struct ProxmoxIsoConfig {
129	/// ISO type; only "qemu" is supported.
130	#[serde(rename = "type")]
131	pub flavor: String,
132	pub storage: String,
133	/// Full Proxmox storage path override, e.g. "storage:iso/my.iso". Empty = use type.
134	pub custom_path: String,
135}
136
137#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
138#[serde(rename_all = "camelCase", deny_unknown_fields)]
139pub struct CloudInitConfig {
140	pub image: String,
141	pub user: String,
142	pub ipconfig0: String,
143	pub ipconfig1: String,
144}
145
146#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct DigitalOceanConfig {
149	pub region: String,
150	pub size: String,
151	pub image: String,
152}
153
154#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
155#[serde(rename_all = "camelCase", deny_unknown_fields)]
156pub struct VmwareConfig {
157	pub vmx_path: String,
158	#[serde(default)]
159	pub iso_directory: String,
160	#[serde(default)]
161	pub cores: u16,
162	#[serde(default, rename = "memoryMiB")]
163	pub memory_mib: u32,
164}
165
166#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
167#[serde(rename_all = "camelCase", deny_unknown_fields)]
168pub struct WslConfig {
169	#[serde(default)]
170	pub enable: bool,
171	#[serde(default)]
172	pub windows_host: String,
173	#[serde(default)]
174	pub windows_user: String,
175	#[serde(default = "default_wsl_distribution")]
176	pub distribution: String,
177	#[serde(default)]
178	pub install_root: String,
179	#[serde(default = "default_wsl_bootstrap_user")]
180	pub bootstrap_user: String,
181	#[serde(default)]
182	pub guest_host: String,
183	#[serde(default = "default_wsl_transport")]
184	pub transport: String,
185}
186
187impl Default for WslConfig {
188	fn default() -> Self {
189		Self {
190			enable: false,
191			windows_host: String::new(),
192			windows_user: String::new(),
193			distribution: default_wsl_distribution(),
194			install_root: String::new(),
195			bootstrap_user: default_wsl_bootstrap_user(),
196			guest_host: String::new(),
197			transport: default_wsl_transport(),
198		}
199	}
200}
201
202fn default_wsl_distribution() -> String {
203	"NixOS".to_string()
204}
205
206fn default_wsl_bootstrap_user() -> String {
207	"nixos".to_string()
208}
209
210fn default_wsl_transport() -> String {
211	"auto".to_string()
212}
213
214#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
215#[serde(rename_all = "camelCase", deny_unknown_fields)]
216pub struct CrossConfig {
217	#[serde(default)]
218	pub local_system: Option<serde_json::Value>,
219	#[serde(default)]
220	pub cross_system: Option<serde_json::Value>,
221}
222
223fn default_build_system() -> bool {
224	true
225}
226
227/// Mirrors the host value behind `deployment.binaryCache`, which is the
228/// projection NXD reads. Consumers assert the two agree.
229#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
230#[serde(rename_all = "camelCase", deny_unknown_fields)]
231pub struct NxdMetadata {
232	#[serde(default, skip_serializing_if = "Option::is_none")]
233	pub binary_cache: Option<BinaryCacheConfig>,
234	/// Secrets-repository site holding this host's material. Consumers author
235	/// identity bindings at evaluation time, before any filesystem lookup, so a
236	/// host outside the repository's own site must declare it.
237	#[serde(default, skip_serializing_if = "String::is_empty")]
238	pub secrets_site: String,
239}
240
241/// Generic host attachment to an optional external provider plane (e.g. client
242/// Tailnet enrollment). Core uses only `enable`, `provider`, and
243/// `secret_binding` for selection, secrets, and composition. `policy` is opaque
244/// provider schema. Control-plane services remain host OS config, not this
245/// attachment.
246#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
247#[serde(rename_all = "camelCase", deny_unknown_fields)]
248pub struct RuntimeSecretInput {
249	/// Operation-private target path. It must be below `/run/nxd/enrollment/`.
250	pub path: String,
251	/// Exact consumer-owned systemd unit that consumes the file.
252	pub activation_unit: String,
253}
254
255#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
256#[serde(rename_all = "camelCase", deny_unknown_fields)]
257pub struct HostPlaneAttachment {
258	#[serde(default)]
259	pub enable: bool,
260	/// Provider instance id (e.g. `provider/headscale`). Required when enable.
261	#[serde(default)]
262	pub provider: String,
263	/// Optional confidential binding for this host/plane.
264	#[serde(default, skip_serializing_if = "String::is_empty")]
265	pub secret_binding: String,
266	/// Optional Nix-owned runtime delivery contract for ephemeral enrollment.
267	/// The plane provider never sees or executes this target-local policy.
268	#[serde(default, skip_serializing_if = "Option::is_none")]
269	pub runtime_secret: Option<RuntimeSecretInput>,
270	/// Provider-owned policy (tags, users, leaf names, etc.). Opaque to core.
271	#[serde(default, skip_serializing_if = "is_null_or_empty_object")]
272	pub policy: serde_json::Value,
273}
274
275fn is_null_or_empty_object(value: &serde_json::Value) -> bool {
276	value.is_null() || value.as_object().is_some_and(|object| object.is_empty())
277}
278
279#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
280#[serde(rename_all = "camelCase", deny_unknown_fields)]
281pub struct FlakeMetadata {
282	pub deployment: DeploymentConfig,
283	#[serde(default, skip_serializing_if = "Option::is_none")]
284	pub evaluation_source: Option<String>,
285	#[serde(default, skip_serializing_if = "Vec::is_empty")]
286	pub evaluation_source_inputs: Vec<String>,
287	#[serde(default, skip_serializing_if = "Option::is_none")]
288	pub evaluation_secret_source: Option<String>,
289	#[serde(default)]
290	pub nxd: NxdMetadata,
291	/// Named host plane attachments (e.g. `tailnet`). Consumer maps site-specific
292	/// authoring (such as LAMT `headscaleEnrollment`) into this common shape.
293	#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
294	pub plane_attachments: BTreeMap<String, HostPlaneAttachment>,
295	pub system: String,
296	#[serde(default, skip_serializing_if = "Option::is_none")]
297	pub system_output: Option<String>,
298	#[serde(default, skip_serializing_if = "Option::is_none")]
299	pub system_derivation: Option<String>,
300	#[serde(default, skip_serializing_if = "Option::is_none")]
301	pub disko_output: Option<String>,
302	pub user: String,
303	pub has_disko: bool,
304	#[serde(default = "default_build_system")]
305	pub build_system: bool,
306	#[serde(default)]
307	pub wsl: bool,
308	#[serde(default)]
309	pub class: String,
310	#[serde(default)]
311	pub server: bool,
312	#[serde(default)]
313	pub home: bool,
314	#[serde(default)]
315	pub role: Option<String>,
316	#[serde(default)]
317	pub tags: Vec<String>,
318	#[serde(default)]
319	pub cross: Option<CrossConfig>,
320	#[serde(default)]
321	pub features: Vec<serde_json::Value>,
322	#[serde(default)]
323	pub os_features: Vec<serde_json::Value>,
324	#[serde(default)]
325	pub hm_features: Vec<serde_json::Value>,
326}