fix: quote quadlet environment values

This commit is contained in:
Dorian
2026-05-14 01:15:22 -04:00
parent be50dc3235
commit f95e9a1cd0
3 changed files with 31 additions and 3 deletions

View File

@@ -222,7 +222,7 @@ impl QuadletUnit {
for env in &self.environment {
// env entries already arrive shaped as "KEY=VALUE"; quadlet
// accepts that form on a single Environment= line per pair.
let _ = writeln!(s, "Environment={env}");
let _ = writeln!(s, "Environment={}", quote_environment(env));
}
for dev in &self.devices {
let _ = writeln!(s, "AddDevice={dev}");
@@ -296,6 +296,19 @@ fn shell_join(parts: &[String]) -> String {
.join(" ")
}
fn quote_environment(env: &str) -> String {
let env = env.replace(['\r', '\n'], " ");
if env.is_empty() || env.chars().any(|c| c.is_whitespace() || "\"\\$`".contains(c)) {
let escaped = env
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "$$");
format!("\"{escaped}\"")
} else {
env
}
}
impl QuadletUnit {
/// Build a backend-flavour QuadletUnit from a parsed AppManifest.
/// Wired through `prod_orchestrator::install_via_quadlet`, gated by
@@ -778,6 +791,19 @@ mod tests {
);
}
#[test]
fn quote_environment_quotes_values_with_spaces() {
assert_eq!(quote_environment("BITCOIN_RPC_PASS=secret"), "BITCOIN_RPC_PASS=secret");
assert_eq!(
quote_environment("RELAY_NAME=Archipelago Nostr Relay"),
"\"RELAY_NAME=Archipelago Nostr Relay\""
);
assert_eq!(
quote_environment("GREETING=say \"hi\""),
"\"GREETING=say \\\"hi\\\"\""
);
}
#[test]
fn restart_policy_emits_correct_systemd_string() {
assert_eq!(RestartPolicy::Always.as_systemd(), "always");
@@ -797,6 +823,7 @@ mod tests {
environment: vec![
"BITCOIN_RPC_USER=archipelago".into(),
"BITCOIN_RPC_PASS=secret".into(),
"RELAY_NAME=Archipelago Nostr Relay".into(),
],
devices: vec!["/dev/kvm".into()],
add_hosts: vec![("host.archipelago".into(), "10.89.0.1".into())],
@@ -813,6 +840,7 @@ mod tests {
assert!(s.contains("PublishPort=8333:8333/tcp"));
assert!(s.contains("Environment=BITCOIN_RPC_USER=archipelago"));
assert!(s.contains("Environment=BITCOIN_RPC_PASS=secret"));
assert!(s.contains("Environment=\"RELAY_NAME=Archipelago Nostr Relay\""));
assert!(s.contains("AddDevice=/dev/kvm"));
assert!(s.contains("AddHost=host.archipelago:10.89.0.1"));
assert!(s.contains("ReadOnly=true"));

View File

@@ -160,7 +160,7 @@ export function getStatusLabel(state: PackageState, health?: string | null, exit
if (state === PackageState.Running && health === 'unhealthy') return 'unhealthy'
if (state === PackageState.Running && health === 'healthy') return 'healthy'
if (state === PackageState.Exited) {
if (exitCode === 137) return 'killed (OOM)'
if (exitCode === 137) return 'killed (SIGKILL)'
if (exitCode != null && exitCode !== 0) return 'crashed'
return 'stopped'
}

View File

@@ -184,7 +184,7 @@ export function getStatusLabel(state: PackageState, health?: string | null, exit
if (state === PackageState.Updating) return 'updating...'
if (state === PackageState.Running) return 'running'
if (state === PackageState.Exited || state === PackageState.Stopped) {
if (exitCode === 137) return 'killed (OOM)'
if (exitCode === 137) return 'killed (SIGKILL)'
if (exitCode != null && exitCode !== 0) return 'crashed'
return 'stopped'
}