k8s: opt-in skip of the externals move for pre-seeded volumes - #434
Draft
thewoolleyman wants to merge 1 commit into
Draft
thewoolleyman wants to merge 1 commit into
thewoolleyman wants to merge 1 commit into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
An opt-in that lets a platform pre-populate the job pod's
externalsvolume and havefs-initskip its move of/home/runner/externals. Default behaviour is unchanged: with the new env unset, the pod spec and thefs-initcommand are byte-for-byte what they are today.Why
fs-initmoves the runner image's externals (the bundled Node runtimes) into theexternalsemptyDiron 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, hookv0.7.0, where the copy is stillcopyExternalsToRootonto 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.mdThis is the skip asked for in #168. It complements #399, which mounts the externals from an image volume (needs the
ImageVolumefeature 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:
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 (noRUNNER_VERSIONenv, no version file or image label; onlybin/Runner.Listener.deps.jsonnames it), so the platform declares it beside its image pin.externalsvolume holds a marker file.externals-seeded-<that version>whose content is that version.With the env set:
externals, it replaces the defaultemptyDirrather than being appended beside it (an appended duplicate is rejected by the API server, and anemptyDircan never hold a seed). If the template supplies none, theemptyDiris used as today.fs-initreceives the version as a container env var and runsif [ "$(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/; fiin 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-initbecause onmainthe destination only exists inside the pod. Onv0.7.0, where the copy targets the runner's work volume, the same check is done by the hook process itself before callingio.cp. A downstream platform runs thatv0.7.0form 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:createJobPoduses them for thefs-initcommand and env, and for theexternalsvolume.packages/k8s/tests/k8s-utils-test.ts: unit tests for the helpers, including running the generatedfs-initscript undershagainst 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-checkandnpm run lint: clean.k8s-utils-test.tspass 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
externalsvolume from the template should stay gated on the env, as here, or be allowed generally) before polishing.