Skip to content

Commit feb85bc

Browse files
andrasbacsaiclaude
andcommitted
fix(builder): use --pipe transient service, not --scope, for sandbox properties
systemd-run --scope adopts an existing process into a .scope unit, and scopes reject service-only assignments like PrivateTmp. First build on the nightly deploy failed with `Unknown assignment: PrivateTmp=yes` because the sandbox properties were all such assignments. Switch to a transient .service unit via --pipe, which still connects stdin/stdout/stderr back to coold (so NDJSON progress parsing keeps working) and accepts the full sandbox set. Add --collect so the unit is released from the runtime state as soon as it exits. Unit name moves from coolify-build-<req>.scope to coolify-build-<req>.service; e2e-mesh.sh updated in the coolify-cli repo to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1024747 commit feb85bc

1 file changed

Lines changed: 26 additions & 19 deletions

File tree

coold/src/builder/mod.rs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
//! Builder dispatch adapter.
22
//!
3-
//! coold spawns one `builder` subprocess per `BuildRequest` under a
4-
//! `systemd-run --scope` transient unit. The scope provides cgroup/FS
5-
//! isolation and, because coold itself runs as a systemd service, is
6-
//! nested under coold's cgroup — coold termination tears every active
7-
//! scope down with it.
3+
//! coold spawns one `builder` subprocess per `BuildRequest` inside a
4+
//! transient systemd service unit via `systemd-run --pipe`. That gives
5+
//! the build a cgroup with memory/CPU caps plus filesystem sandboxing
6+
//! (PrivateTmp, ProtectSystem=strict, ReadWritePaths allowlist) while
7+
//! still piping stdout/stderr back to coold so the NDJSON event frames
8+
//! can be parsed in real time.
89
//!
9-
//! Cancellation is wired through `systemctl kill` against the scope
10-
//! unit name. The same mechanism kills `buildah` and `git` children in
11-
//! a single sweep via the cgroup.
10+
//! (A `systemd-run --scope` was the original design but scopes are
11+
//! process-adoption wrappers and reject service-only properties like
12+
//! PrivateTmp — building with them errors `Unknown assignment:
13+
//! PrivateTmp=yes`. Transient services accept the full sandbox set.)
14+
//!
15+
//! Cancellation uses `systemctl kill --signal=SIGTERM` against the
16+
//! transient unit name; the cgroup kill takes `buildah` and `git`
17+
//! children down in the same sweep.
1218
1319
use std::collections::HashMap;
1420
use std::path::PathBuf;
@@ -37,7 +43,7 @@ pub struct BuilderCtx {
3743
}
3844

3945
struct BuildHandle {
40-
scope_unit: String,
46+
unit_name: String,
4147
}
4248

4349
pub struct BuilderSettings {
@@ -99,25 +105,25 @@ impl BuilderCtx {
99105
/// reported via the normal outbound response path once the subprocess
100106
/// exits.
101107
pub async fn cancel(&self, request_id: &str) -> bool {
102-
let scope_unit = match self.active.lock().await.get(request_id) {
103-
Some(h) => h.scope_unit.clone(),
108+
let unit_name = match self.active.lock().await.get(request_id) {
109+
Some(h) => h.unit_name.clone(),
104110
None => return false,
105111
};
106112
match Command::new("systemctl")
107-
.args(["kill", "--signal=SIGTERM", &scope_unit])
113+
.args(["kill", "--signal=SIGTERM", &unit_name])
108114
.status()
109115
.await
110116
{
111117
Ok(s) if s.success() => {
112-
info!(%request_id, %scope_unit, "cancel SIGTERM sent");
118+
info!(%request_id, %unit_name, "cancel SIGTERM sent");
113119
true
114120
}
115121
Ok(s) => {
116-
warn!(%request_id, %scope_unit, status = ?s, "systemctl kill non-zero");
122+
warn!(%request_id, %unit_name, status = ?s, "systemctl kill non-zero");
117123
true
118124
}
119125
Err(e) => {
120-
warn!(%request_id, %scope_unit, error = %e, "systemctl kill failed");
126+
warn!(%request_id, %unit_name, error = %e, "systemctl kill failed");
121127
false
122128
}
123129
}
@@ -136,11 +142,12 @@ impl BuilderCtx {
136142
.await
137143
.map_err(|e| build_err(500, "setup", format!("write request.json: {e}")))?;
138144

139-
let scope_unit = format!("coolify-build-{request_id}.scope");
145+
let unit_name = format!("coolify-build-{request_id}.service");
140146
let mut cmd = Command::new("systemd-run");
141-
cmd.arg("--scope")
147+
cmd.arg("--pipe")
142148
.arg("--quiet")
143-
.arg(format!("--unit={scope_unit}"))
149+
.arg("--collect")
150+
.arg(format!("--unit={unit_name}"))
144151
.arg("-p")
145152
.arg(format!("RuntimeMaxSec={}", self.timeout_secs))
146153
.arg("-p")
@@ -168,7 +175,7 @@ impl BuilderCtx {
168175
self.active
169176
.lock()
170177
.await
171-
.insert(request_id.to_string(), BuildHandle { scope_unit: scope_unit.clone() });
178+
.insert(request_id.to_string(), BuildHandle { unit_name: unit_name.clone() });
172179

173180
let outcome = self.spawn_and_reap(&mut cmd, request_id, &work_dir).await;
174181

0 commit comments

Comments
 (0)