diff options
| author | tv <tv@krebsco.de> | 2026-11-13 20:36:51 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-13 22:01:56 +0100 |
| commit | 68e42cf9b0ca5a512500e7dc524183872b3ebaa5 (patch) | |
| tree | 7c375d54f1506acf96a2fb68834da9ae2c7f5715 /pkgs | |
| parent | ce2cd2553a937d84f1a78386ab28502e89f247d6 (diff) | |
populate: decide whether a target is local like isLocalTarget
For specs whose target's local is null, the program decides whether the
target is local like krops' lib's isLocalTarget: an empty LOGNAME or
HOSTNAME counts as unset, as with lib.maybeEnv, and /etc/hostname's
labels can be of any length, as with lib.types.label. is_local_with,
pure, has unit tests.
Diffstat (limited to 'pkgs')
| -rw-r--r-- | pkgs/populate/docs/design.md | 8 | ||||
| -rw-r--r-- | pkgs/populate/src/run.rs | 91 |
2 files changed, 86 insertions, 13 deletions
diff --git a/pkgs/populate/docs/design.md b/pkgs/populate/docs/design.md index cc78a65..639d15b 100644 --- a/pkgs/populate/docs/design.md +++ b/pkgs/populate/docs/design.md @@ -153,9 +153,11 @@ places populate must not touch, check this after every phase. ## Execution Whether a target is local is what the target's `local` says, or, if -that's null, gets decided when the program runs: it is, if its user is -`LOGNAME` and its host is `localhost` or the host's name, from `HOSTNAME` -or `/etc/hostname`. `default.nix` decides during evaluation, like the other +that's null, gets decided when the program runs, like krops' lib's +`isLocalTarget` does: it is, if its user is `LOGNAME`, or null if that's +empty or unset, and its host is `localhost` or the host's name, from +`HOSTNAME`, or, if that's empty or unset, the only line of `/etc/hostname` +that is a label. `default.nix` decides during evaluation, like the other functions of krops. For a local target, populate runs commands directly. For a remote target, populate runs commands over ssh, sharing one master diff --git a/pkgs/populate/src/run.rs b/pkgs/populate/src/run.rs index 4ed1d76..a16ffe4 100644 --- a/pkgs/populate/src/run.rs +++ b/pkgs/populate/src/run.rs @@ -359,25 +359,48 @@ pub fn output(command: &mut Command) -> Result<String> { } /// Whether the target is the machine populate runs on, as the user running -/// it: its user is that user, and its host is localhost or the machine's -/// name, like krops' lib's isLocalTarget decides during evaluation. +/// it, like krops' lib's isLocalTarget decides during evaluation. fn is_local(target: &Target) -> bool { - let user = std::env::var("LOGNAME").ok(); - let host = std::env::var("HOSTNAME").ok().or_else(host_name); - target.user == user && (target.host == "localhost" || Some(&target.host) == host.as_ref()) + let etc_hostname = fs::read_to_string("/etc/hostname").ok(); + is_local_with( + target, + std::env::var("LOGNAME").ok().as_deref(), + std::env::var("HOSTNAME").ok().as_deref(), + etc_hostname.as_deref(), + ) } -/// The machine's name, from /etc/hostname, as krops' lib reads it. -fn host_name() -> Option<String> { - let text = fs::read_to_string("/etc/hostname").ok()?; +/// is_local, given LOGNAME, HOSTNAME, and the contents of /etc/hostname. +/// Like lib.maybeEnv, an empty variable counts as unset. The user running +/// populate is LOGNAME, if set, and the machine's name is HOSTNAME, or else +/// the only line of /etc/hostname that is a label, or else localhost. The +/// target is local if its user is that user, and its host is that name or +/// localhost. +fn is_local_with( + target: &Target, + logname: Option<&str>, + hostname: Option<&str>, + etc_hostname: Option<&str>, +) -> bool { + let user = logname.filter(|v| !v.is_empty()); + let host = hostname + .filter(|v| !v.is_empty()) + .map(str::to_string) + .or_else(|| etc_hostname.and_then(host_name)) + .unwrap_or_else(|| "localhost".to_string()); + target.user.as_deref() == user && (target.host == "localhost" || target.host == host) +} + +/// The machine's name in the contents of /etc/hostname: its only line that +/// is a label, as krops' lib reads it. +fn host_name(text: &str) -> Option<String> { let label = |line: &&str| { !line.is_empty() - && line.len() <= 63 && line.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') && !line.starts_with('-') && !line.ends_with('-') }; - let names: Vec<&str> = text.lines().filter(label).collect(); + let names: Vec<&str> = text.split('\n').filter(label).collect(); match names.as_slice() { [name] => Some(name.to_string()), _ => None, @@ -401,6 +424,54 @@ mod tests { assert_eq!(quote("it's"), r"'it'\''s'"); } + fn target(user: Option<&str>, host: &str) -> Target { + serde_json::from_value(serde_json::json!({ + "user": user, "host": host, "path": "/var/src" + })) + .unwrap() + } + + #[test] + fn local_targets() { + let local = |t: &Target, logname, hostname, etc| is_local_with(t, logname, hostname, etc); + // An empty LOGNAME counts as unset, i.e. the user null. + assert!(local(&target(None, "localhost"), Some(""), None, None)); + assert!(!local(&target(None, "localhost"), Some("tv"), None, None)); + assert!(local( + &target(Some("tv"), "localhost"), + Some("tv"), + None, + None + )); + // An empty HOSTNAME counts as unset, so /etc/hostname names the host. + assert!(local( + &target(Some("tv"), "ni"), + Some("tv"), + Some(""), + Some("ni\n") + )); + assert!(local( + &target(Some("tv"), "ni"), + Some("tv"), + Some("ni"), + Some("other\n") + )); + assert!(!local( + &target(Some("tv"), "ni"), + Some("tv"), + None, + Some("a\nb\n") + )); + // Labels of any length, like lib.types.label. + let long = "x".repeat(64); + assert!(local( + &target(Some("tv"), &long), + Some("tv"), + None, + Some(&long) + )); + } + #[test] fn store_paths() { assert!(is_store_path("/nix/store/abc-x")); |
