Skip to content

Commit b3fdb17

Browse files
committed
test(e2e): migrate builder suite to Hetzner-provisioned cluster
Replace pre-existing cluster requirement with ephemeral 2-VM Hetzner setup (A = central + builder, B = coold-only). All scenarios collapsed into single `builder_lifecycle` test so one cluster is reused across every dispatch/cancel/restart/artifact-perm scenario. Drop manual BUILDER_HOST/COOLD_ONLY_HOST/BUILDER_MGMT/COOLD_ONLY_MGMT/ CENTRAL_HOST env vars — now driven by same HETZNER_TOKEN/PROJECT/SSH_KEY vars as the install suite. Update .env.example and E2E.md accordingly.
1 parent 0c0384d commit b3fdb17

4 files changed

Lines changed: 210 additions & 148 deletions

File tree

e2e-tests/.env.example

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copy to .env and fill in. Never committed — root .gitignore covers it.
22
# Values only apply when not already exported in the shell.
33

4-
# ─── Install suite (Hetzner-provisioned) ─────────────────────────────────────
4+
# ─── Hetzner-provisioned suites (install + builder) ──────────────────────────
55
HETZNER_TOKEN=
66
HETZNER_PROJECT=my-coold-e2e
77
SSH_KEY=/Users/you/.ssh/id_ed25519
@@ -10,13 +10,5 @@ COOLIFY_BIN=coolify
1010
# HETZNER_IMAGE=ubuntu-24.04
1111
# HETZNER_SERVER_TYPE=cx23
1212

13-
# ─── Builder suite (pre-existing cluster) ────────────────────────────────────
14-
# BUILDER_HOST=
15-
# COOLD_ONLY_HOST=
16-
# BUILDER_MGMT=
17-
# COOLD_ONLY_MGMT=
18-
# CENTRAL_HOST=
19-
# SSH_USER=root
20-
2113
# ─── Sweeper opt-in ──────────────────────────────────────────────────────────
2214
# CONFIRM_SWEEP=1

e2e-tests/E2E.md

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,20 @@ $EDITOR e2e-tests/.env
1919
cargo test -p e2e-tests --no-run
2020
```
2121

22-
## Suite 1 — `builder.rs` (pre-installed cluster)
22+
## Suite 1 — `builder.rs` (Hetzner-provisioned)
2323

24-
Requires an already-bootstrapped cluster. Env vars:
24+
Provisions 2 VMs (A = central + builder, B = coold-only), runs
25+
`coolify init apply`, then executes every dispatch / cancel / restart
26+
/ artifact-perm scenario on the shared cluster. VMs destroyed on drop.
27+
Uses the same env vars as the install suite (see below):
2528

2629
```bash
27-
export BUILDER_HOST=<ssh-addr-of-builder-host>
28-
export COOLD_ONLY_HOST=<ssh-addr-of-coold-only-host>
29-
export BUILDER_MGMT=<wg0-ip-of-builder-host>
30-
export COOLD_ONLY_MGMT=<wg0-ip-of-coold-only-host>
31-
export CENTRAL_HOST=<ssh-addr-of-central>
32-
export SSH_KEY=~/.ssh/<key>
33-
# optional:
34-
export SSH_USER=root
35-
```
36-
37-
### Run all in suite
38-
39-
```bash
40-
cargo test -p e2e-tests --test builder -- --ignored --nocapture --test-threads=1
30+
cargo test -p e2e-tests --test builder builder_lifecycle -- --ignored --nocapture --test-threads=1
4131
```
4232

43-
### Individual tests
44-
45-
```bash
46-
cargo test -p e2e-tests --test builder pin_to_builder_host -- --ignored --nocapture
47-
cargo test -p e2e-tests --test builder pin_to_coold_only_host_returns_503 -- --ignored --nocapture
48-
cargo test -p e2e-tests --test builder unknown_host_id_returns_503 -- --ignored --nocapture
49-
cargo test -p e2e-tests --test builder load_balance_picks_builder_host -- --ignored --nocapture
50-
cargo test -p e2e-tests --test builder build_cancel_emits_stage_cancel -- --ignored --nocapture
51-
cargo test -p e2e-tests --test builder coold_restart_adopts_in_flight_build -- --ignored --nocapture
52-
```
33+
The whole suite is a single `#[test] fn builder_lifecycle` — there are
34+
no longer individual scenario tests (running each separately would
35+
provision its own cluster, which is wasteful).
5336

5437
## Suite 2 — `install.rs` (Hetzner-provisioned)
5538

e2e-tests/src/lib.rs

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
//! Live-server test harness for the coold/broker/builder stack.
22
//!
3-
//! Tests are written as Rust integration tests under `tests/`, marked
4-
//! `#[ignore]` so default `cargo test` skips them. Run with:
3+
//! Tests are Rust integration tests under `tests/`, marked `#[ignore]` so
4+
//! default `cargo test` skips them. Every suite provisions its own
5+
//! ephemeral Hetzner cluster via [`hetzner::EphemeralCluster`], runs
6+
//! `coolify init apply` from the local `coolify` binary, then exercises
7+
//! the black-box HTTP/UDS/systemd contract over SSH. No broker/coold
8+
//! code is linked.
9+
//!
10+
//! Run with:
511
//!
612
//! ```text
7-
//! BUILDER_HOST=<host-a> \
8-
//! COOLD_ONLY_HOST=<host-b> \
9-
//! BUILDER_MGMT=<wg0-ip-of-host-a> \
10-
//! COOLD_ONLY_MGMT=<wg0-ip-of-host-b> \
11-
//! CENTRAL_HOST=<central-host> \
13+
//! HETZNER_TOKEN=... HETZNER_PROJECT=... \
1214
//! SSH_KEY=~/.ssh/<key> \
13-
//! cargo test -p e2e-tests -- --ignored --test-threads=1
15+
//! COOLIFY_BIN=$(which coolify) \
16+
//! cargo test -p e2e-tests -- --ignored --nocapture --test-threads=1
1417
//! ```
1518
//!
16-
//! `--test-threads=1` is mandatory: the tests dispatch real builds against
17-
//! a shared cluster, and running them in parallel overwhelms the
18-
//! `COOLD_BUILDER_CAPACITY` semaphore and races on `buildah images` state
19-
//! shared across hosts.
20-
//!
21-
//! The harness drives the broker UDS via `ssh + curl --unix-socket` on the
22-
//! central host and asserts remote state via `buildah images` and
23-
//! `systemctl is-active`. No broker/coold code is linked — tests exercise
24-
//! the black-box contract.
19+
//! `--test-threads=1` is mandatory: the tests dispatch real builds and
20+
//! running them in parallel overwhelms the `COOLD_BUILDER_CAPACITY`
21+
//! semaphore and races on `buildah images` state shared across hosts.
22+
//! VMs are deleted via [`hetzner::EphemeralCluster`]'s RAII `Drop`, so
23+
//! panics during assertions still clean up paid resources.
2524
2625
use std::process::Command;
2726
use std::thread;
@@ -90,37 +89,34 @@ pub struct Env {
9089
pub cool_only_mgmt: String,
9190
pub central_host: String,
9291
pub ssh_key: String,
93-
pub ssh_user: String,
9492
}
9593

9694
impl Env {
97-
pub fn from_env() -> Self {
98-
load_dotenv();
95+
/// Build from a live [`hetzner::EphemeralCluster`] provisioned with
96+
/// 2 hosts (A = central + builder, B = coold-only) and the wg0
97+
/// addresses resolved on each host after `coolify init apply`.
98+
pub fn from_cluster(
99+
cluster: &hetzner::EphemeralCluster,
100+
builder_mgmt: String,
101+
cool_only_mgmt: String,
102+
) -> Self {
103+
let hosts = cluster.hosts();
104+
assert_eq!(hosts.len(), 2, "Env::from_cluster expects 2 hosts");
99105
Self {
100-
builder_host: must("BUILDER_HOST"),
101-
cool_only_host: must("COOLD_ONLY_HOST"),
102-
builder_mgmt: must("BUILDER_MGMT"),
103-
cool_only_mgmt: must("COOLD_ONLY_MGMT"),
104-
central_host: must("CENTRAL_HOST"),
105-
ssh_key: must("SSH_KEY"),
106-
ssh_user: std::env::var("SSH_USER").unwrap_or_else(|_| "root".into()),
106+
builder_host: hosts[0].ipv4.clone(),
107+
cool_only_host: hosts[1].ipv4.clone(),
108+
builder_mgmt,
109+
cool_only_mgmt,
110+
central_host: hosts[0].ipv4.clone(),
111+
ssh_key: cluster.ssh_key.clone(),
107112
}
108113
}
109114

110115
pub fn ssh(&self, host: &str, cmd: &str) -> Result<String, String> {
111116
let out = Command::new("ssh")
112-
.args([
113-
"-i",
114-
&self.ssh_key,
115-
"-o",
116-
"StrictHostKeyChecking=accept-new",
117-
"-o",
118-
"BatchMode=yes",
119-
"-o",
120-
"ConnectTimeout=10",
121-
&format!("{}@{}", self.ssh_user, host),
122-
cmd,
123-
])
117+
.args(hetzner::ephemeral_ssh_args(&self.ssh_key))
118+
.arg(format!("root@{host}"))
119+
.arg(cmd)
124120
.output()
125121
.map_err(|e| format!("spawn ssh: {e}"))?;
126122
if !out.status.success() {
@@ -267,10 +263,6 @@ impl Env {
267263
}
268264
}
269265

270-
fn must(key: &str) -> String {
271-
std::env::var(key).unwrap_or_else(|_| panic!("env {key} required"))
272-
}
273-
274266
/// Lowercase request_id suitable for use as an OCI image tag (OCI rejects
275267
/// uppercase in repository names).
276268
pub fn uniq_req_id(prefix: &str) -> String {

0 commit comments

Comments
 (0)