Skip to content

ci: gate the exec path on a real container, no KVM required - #205

Merged
dshakes merged 3 commits into
masterfrom
ci/exec-e2e-gate
Jul 31, 2026
Merged

dshakes merged 3 commits into
masterfrom
ci/exec-e2e-gate

Conversation

@dshakes

@dshakes dshakes commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Item #2 from the remaining-work list. The exec e2e test has existed since the exec dispatch fix but only ran by hand (LANTERN_RUNTIME_E2E=1), so the regression it guards was unprotected.

That regression is worth remembering: grpcSchedulerClient.Exec returned a canned string with exit code 0, making a missing implementation indistinguishable from a command that ran and printed nothing. Mocks can't catch that — only a real workload can.

Why this needs no KVM

The runtime-manager's docker backend spawns a real container, and hosted runners have Docker. So this is an ordinary PR gate, while the Firecracker boot job still waits on a nested-virt runner. The job builds the manager and scheduler, starts both, and runs the test against them.

No Postgres service needed — with DATABASE_URL unset the scheduler uses its in-memory store and runs always-leader, which is exactly what a single-node test wants.

The gate asserts the test actually RAN

go test reports a skipped test as a pass. Without this the job would go green while proving nothing — the same failure mode as the boot assertion that reported PASS for kernel-panicking VMs all session. So the step:

  • fails on any --- SKIP
  • requires the --- PASS: TestRuntimeExecE2E_RealVM line

Verified both directions before pushing

env unset test SKIPs → gate correctly FAILS
env set 2/2 subtests PASS against a live manager + scheduler → gate passes

The test's own cleanup left no containers behind. Scheduler build command and workflow YAML both checked separately.

What this covers

The 6 subtests: stdout is real command output, the command runs inside the container, nonzero exit propagates, stderr stays separate, filesystem writes persist across execs, and an unknown vm_id errors instead of reporting success.

Workflow-only change.

The exec e2e test has existed since the exec dispatch fix but only ran by hand
(`LANTERN_RUNTIME_E2E=1`), so the regression it guards was unprotected: for a
long time `grpcSchedulerClient.Exec` returned a canned string with exit code 0,
making a MISSING implementation indistinguishable from a command that ran and
printed nothing. Mocks cannot catch that — only a real workload can.

This runs on a stock GitHub runner. The runtime-manager's `docker` backend
spawns a real container and hosted runners have Docker, so no nested virt is
needed; that is why this can be an ordinary PR gate while the Firecracker boot
job still waits on a KVM runner. The job builds the manager and scheduler,
starts both, and runs the test against them.

No Postgres service is needed: with DATABASE_URL unset the scheduler uses its
in-memory store and runs always-leader, which is exactly a single-node test.

The gate asserts the test RAN. `go test` reports a skipped test as a pass, so
without this the job would go green while proving nothing — the same failure
mode as the boot assertion that reported PASS for kernel-panicking VMs. The
step fails on any `--- SKIP` and requires the PASS line for the real-VM test.

Verified both directions locally before pushing:
  - env unset  -> test SKIPs -> gate correctly FAILS
  - env set    -> 2/2 subtests PASS against a live manager + scheduler -> gate
                  passes, and the test's own cleanup left no containers behind
Scheduler build command and workflow YAML both checked.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Codex cross-audit (agent:audit)

Blocking

  1. runtime-exec-e2e.yml:61 builds services/runtime-manager, but the workflow never installs protoc. runtime-manager/build.rs calls tonic_build::compile_protos(...) build.rs:11, and existing QA/lint workflows install protobuf-compiler for this reason. On a clean hosted runner this build step fails before the e2e can run.

  2. runtime-exec-e2e.yml:72 starts the manager without SCHEDULER_URL, so it never self-registers with the scheduler. The manager heartbeat explicitly returns when scheduler_url is empty scheduler_heartbeat.rs:195, while scheduler placement fails if no nodes are registered placement.go:35. After fixing protoc, Schedule should fail with “no nodes registered in cluster” instead of spawning the container. Set SCHEDULER_URL=http://localhost:8085 for the manager, or seed a node another way before running the test.

No other PR-introduced security/correctness issues found in the added workflow.

@compass-sdlc-bot compass-sdlc-bot Bot added the agent:reviewed-clean Reviewer found no Blocking issues this round label Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔷 Gemini cross-audit (agent:audit-gemini)

Audit Report: pr-diff.patch


🚨 Blocking Issues

1. Misconfigured GHA Workflow: Missing SCHEDULER_URL for runtime-manager Node Registration (Correctness/Failure)

  • Impact: The E2E test suite will fail completely under CI.
  • Details:
    • In the Start runtime-manager (docker backend) step of .github/workflows/runtime-exec-e2e.yml, the environment variable SCHEDULER_URL is not provided to the lantern-runtime-manager process.
    • Without SCHEDULER_URL configured, the runtime-manager's background heartbeat loop in services/runtime-manager/src/scheduler_heartbeat.rs skips execution:
      if cfg.scheduler_url.is_empty() {
          tracing::info!("scheduler_url not set; skipping self-register heartbeat");
          return;
      }
    • As a result, the node never registers with the runtime-scheduler (which listens on HTTP :8085).
    • When the Go E2E test calls Schedule, the scheduler's placement engine (Pick) will have 0 registered nodes, failing immediately with "no nodes registered in cluster":
      nodes := e.Store.ListNodes()
      if len(nodes) == 0 {
          return nil, fmt.Errorf("no nodes registered in cluster")
      }
  • Resolution: In the workflow, set SCHEDULER_URL to point to the scheduler's HTTP endpoint during the manager's startup step:
          - name: Start runtime-manager (docker backend)
            run: |
              set -euo pipefail
              RUNTIME_BACKEND=docker \
              LISTEN_ADDR=0.0.0.0:50054 \
              SCHEDULER_URL=http://localhost:8085 \
              AGENT_IMAGE=python:3.11-slim \
              LOG_LEVEL=info \
              ./services/runtime-manager/target/debug/lantern-runtime-manager \
                > /tmp/manager.log 2>&1 &

🔍 Other Observations & Missed Edge Cases

2. Hardcoded Rust Toolchain Version vs. rust-toolchain.toml

  • Impact: Minor maintenance/future-proofing concern.
  • Details: The workflow set up Rust step specifies toolchain: "1.93". While this is consistent with the current rust-toolchain.toml value, it introduces a duplicate source of truth. If the project updates the toolchain in rust-toolchain.toml, this workflow will continue to compile with 1.93 instead of the upgraded toolchain, violating the step's name: Set up Rust (pinned by rust-toolchain.toml).
  • Resolution: Omit the explicit toolchain key under dtolnay/rust-toolchain to let it automatically parse and honor rust-toolchain.toml.

3. Bash-Specific /dev/tcp Network Redirections

  • Impact: Robustness.
  • Details: The startup readiness check uses the bash-specific net redirection (exec 3<>/dev/tcp/127.0.0.1/<port>). While standard GitHub hosted Ubuntu runners have bash and net redirections enabled, this fails under pure sh environments or non-standard shells.
  • Resolution: Standard tools like nc -z localhost <port> or standard polling using curl are more robust and portable.

dshakes added 2 commits July 31, 2026 08:17
The runtime-manager's build.rs compiles the runtime proto with prost-build,
which shells out to protoc. Hosted runners do not ship it, so the build failed
with 'Could not find protoc' — a dev Mac has it, which is exactly why this only
appeared once the job ran in real CI rather than in local rehearsal. Same step
sdlc-qa already uses.
The test failed with 'placement failed: no nodes registered in cluster'. The
manager self-registers with the scheduler via SCHEDULER_URL (POST
/v1/nodes/heartbeat), and the workflow never set it — the LaunchAgent setup
this was rehearsed against already had it, which is exactly why the gap only
surfaced in real CI.

Scheduler now starts FIRST so registration lands immediately, the manager gets
SCHEDULER_URL and a stable NODE_NAME, and a readiness step waits for the node
instead of racing it.

That step watches the manager's own log rather than GET /v1/cluster: the
endpoint requires an Authorization header, so polling it would 401 for the full
timeout and report nothing useful. It also fails fast on 'heartbeat rejected' /
'heartbeat send failed' instead of waiting out the timeout. Verified locally
that 'heartbeat ok' really is emitted under
RUST_LOG=...scheduler_heartbeat=debug before relying on it.
@dshakes
dshakes merged commit 16bce93 into master Jul 31, 2026
7 checks passed
@dshakes
dshakes deleted the ci/exec-e2e-gate branch August 4, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:reviewed-clean Reviewer found no Blocking issues this round

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant