Skip to content

k8s: opt-in skip of the externals move for pre-seeded volumes - #434

Draft
thewoolleyman wants to merge 1 commit into
actions:mainfrom
thewoolleyman:externals-preseeded-skip
Draft

thewoolleyman wants to merge 1 commit into
actions:mainfrom
thewoolleyman:externals-preseeded-skip

Conversation

@thewoolleyman

Copy link
Copy Markdown

What

An opt-in that lets a platform pre-populate the job pod's externals volume and have fs-init skip its move of /home/runner/externals. Default behaviour is unchanged: with the new env unset, the pod spec and the fs-init command are byte-for-byte what they are today.

Why

fs-init moves the runner image's externals (the bundled Node runtimes) into the externals emptyDir on every job pod. On a self-hosted cluster whose pods are backed by persistent storage, that is a fixed per-job startup cost that does not depend on what the job does.

Measured on one such cluster (runner 2.336.0, hook v0.7.0, where the copy is still copyExternalsToRoot onto the work volume): 595 MB in 9,028 files, about 14 s per job start. For a lint job whose steps took 38 s, that copy was a quarter of the pod's 56 s lifetime. The measurement is written up here: https://github.com/thewoolleyman/livespec/blob/master/plan/ci-runner-pod-lifecycle-reliability/research/005-start-burst-measurement-and-mitigation.md

This is the skip asked for in #168. It complements #399, which mounts the externals from an image volume (needs the ImageVolume feature gate, Kubernetes 1.35+); this proposal is for platforms that can pre-seed a volume themselves (for example by hardlink from a node-local copy of the pinned runner image's externals, when the volume is provisioned) and run on clusters without image volumes.

How

Two things have to hold for the move to be skipped; either one alone changes nothing:

  1. ACTIONS_RUNNER_PRESEEDED_EXTERNALS_VERSION=<runner version> is set on the runner. The version has to come from the environment because the runner exports nothing a hook could read it from (no RUNNER_VERSION env, no version file or image label; only bin/Runner.Listener.deps.json names it), so the platform declares it beside its image pin.
  2. The mounted externals volume holds a marker file .externals-seeded-<that version> whose content is that version.

With the env set:

  • If the hook template supplies a volume named externals, it replaces the default emptyDir rather than being appended beside it (an appended duplicate is rejected by the API server, and an emptyDir can never hold a seed). If the template supplies none, the emptyDir is used as today.
  • fs-init receives the version as a container env var and runs
    if [ "$(cat "/mnt/externals/.externals-seeded-$ACTIONS_RUNNER_PRESEEDED_EXTERNALS_VERSION" 2>/dev/null)" = "$ACTIONS_RUNNER_PRESEEDED_EXTERNALS_VERSION" ]; then echo ...; else mv /home/runner/externals/* /mnt/externals/; fi
    in place of the bare mv. The version is read from the environment inside the script rather than interpolated into it, so nothing needs shell quoting. A missing marker, or one for another version, falls through to the move; the job runs, just without the saving.

The marker check lives in fs-init because on main the destination only exists inside the pod. On v0.7.0, where the copy targets the runner's work volume, the same check is done by the hook process itself before calling io.cp. A downstream platform runs that v0.7.0 form in production today and would drop its patch once an equivalent lands here.

Changes

  • packages/k8s/src/k8s/utils.ts: ENV_PRESEEDED_EXTERNALS_VERSION, preseededExternalsVersion(), externalsInitCommand(), extensionSuppliesVolume().
  • packages/k8s/src/k8s/index.ts: createJobPod uses them for the fs-init command and env, and for the externals volume.
  • packages/k8s/tests/k8s-utils-test.ts: unit tests for the helpers, including running the generated fs-init script under sh against a temp directory to check the four marker states (absent, other version, wrong content, match).
  • packages/k8s/README.md: documents the opt-in.

Testing

  • npm ci && npm run bootstrap && npm run build-all: clean.
  • npm run format-check and npm run lint: clean.
  • The new unit tests in k8s-utils-test.ts pass locally. The rest of the k8s suite needs a kind cluster, which I did not run locally; CI will.

Opened as a draft to get direction on the shape (in particular whether replacing the externals volume from the template should stay gated on the env, as here, or be allowed generally) before polishing.

On every job the fs-init init container moves /home/runner/externals
(the bundled Node runtimes, ~600 MB in ~9,000 files) into the job pod's
externals emptyDir. On self-hosted clusters backed by slower persistent
storage that move is a fixed startup cost paid by every job regardless
of what the job does.

Add an opt-in that lets a platform supply the externals volume itself,
already populated, and have fs-init skip the move:

- ACTIONS_RUNNER_PRESEEDED_EXTERNALS_VERSION=<runner version> on the
  runner names the version the seed was taken from (the runner exports
  no version a hook could read).
- The platform supplies a volume named `externals` through the hook
  template; when the env is set, that volume replaces the default
  emptyDir instead of being appended beside it.
- fs-init checks for `.externals-seeded-<version>` with that version as
  its content inside the mounted volume and skips the move only on a
  match. The version reaches the script through the init container's
  environment, so nothing is quoted into the command.

With the env unset the pod spec and the fs-init command are unchanged.
With the env set but no `externals` volume in the template, or a
missing/mismatched marker, the emptyDir is used and the move runs.
thewoolleyman-factory-bot Bot pushed a commit to thewoolleyman/livespec-dev-tooling that referenced this pull request Sep 6, 2026
…er-hooks#434) and the measured ~14 s per-start externals copy (livespec-wm7c)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
thewoolleyman-factory-bot Bot pushed a commit to thewoolleyman/livespec-dev-tooling that referenced this pull request Sep 6, 2026
…er-hooks#434) and the measured ~14 s per-start externals copy (livespec-wm7c)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant