Skip to content

Commit fa3733e

Browse files
feat(nickel): emit users block as empty-array placeholder (#63)
ProbedState carries no user inventory (Plan §6.7), so reconcile cannot enumerate `/etc/passwd` without flooding the imported.ncl with system accounts. Emit `users = []` after `services` so the operator hand-edits UserDecl entries during review. Block ordering matches DeclaredState's TOML field order for stable golden-fixture diffs. Refs: PRD §11 (reconcile), PRD §17.1 (probed users a v1.1 candidate) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 571b5e5 commit fa3733e

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pearlite-nickel/src/emit.rs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,24 @@
99
//!
1010
//! ## Scope (this chunk)
1111
//!
12-
//! `meta`, `kernel`, `packages`, and `services` are emitted. Unprobed
13-
//! `meta` fields (`timezone`, `arch_level`, `locale`, `keymap`) emit
14-
//! conservative defaults that the operator hand-edits after reconcile
15-
//! lands the file at `<config_dir>/hosts/<host>.imported.ncl`.
16-
//! Subsequent chunks add the `users` and config-file blocks.
12+
//! `meta`, `kernel`, `packages`, `services`, and `users` are emitted.
13+
//! Unprobed `meta` fields (`timezone`, `arch_level`, `locale`,
14+
//! `keymap`) emit conservative defaults that the operator hand-edits
15+
//! after reconcile lands the file at
16+
//! `<config_dir>/hosts/<host>.imported.ncl`. Subsequent chunks add the
17+
//! config-file block.
18+
//!
19+
//! ## Users-block caveat
20+
//!
21+
//! [`ProbedState`] carries no user inventory — Pearlite manages
22+
//! declared users (Plan §6.7) but does not enumerate `/etc/passwd` at
23+
//! probe time, since reconcile-without-declarations would surface
24+
//! every system account (root, daemon, http, etc.) as drift. The
25+
//! emitter therefore writes `users = []` as a placeholder; the
26+
//! operator is expected to add `UserDecl` entries by hand after the
27+
//! initial import. A populated users inventory is a v1.1 candidate
28+
//! (PRD §17.1) and would slot in here without changing the block
29+
//! shape.
1730
//!
1831
//! ## Services-block caveat
1932
//!
@@ -75,10 +88,15 @@ pub fn emit_host(probed: &ProbedState) -> String {
7588
push_kernel(&mut out, &probed.kernel.package);
7689
push_packages(&mut out, probed.pacman.as_ref(), probed.cargo.as_ref());
7790
push_services(&mut out, probed.services.as_ref());
91+
push_users(&mut out);
7892
out.push_str("}\n");
7993
out
8094
}
8195

96+
fn push_users(out: &mut String) {
97+
out.push_str(" users = [],\n");
98+
}
99+
82100
fn push_meta(out: &mut String, hostname: &str) {
83101
out.push_str(" meta = {\n");
84102
push_field(out, "hostname", hostname);
@@ -445,6 +463,28 @@ mod tests {
445463
);
446464
}
447465

466+
#[test]
467+
fn emit_users_emits_empty_array_placeholder() {
468+
// ProbedState carries no user inventory, so the emitter writes
469+
// a literal empty array — the operator hand-edits after the
470+
// imported.ncl lands.
471+
let out = emit_host(&probed_with("forge", "linux-cachyos"));
472+
assert!(out.contains("users = [],"));
473+
}
474+
475+
#[test]
476+
fn emit_users_appears_after_services() {
477+
let mut probed = probed_with("forge", "linux-cachyos");
478+
probed.services = Some(make_services(&["sshd.service"], &[], &[]));
479+
let out = emit_host(&probed);
480+
let services_at = out.find("services = ").expect("services block");
481+
let users_at = out.find("users = ").expect("users block");
482+
assert!(
483+
services_at < users_at,
484+
"users must follow services for stable golden-fixture diffs"
485+
);
486+
}
487+
448488
#[test]
449489
fn emit_meta_block_appears_before_kernel_block() {
450490
// Stable ordering matters for operator review (and for

0 commit comments

Comments
 (0)