Skip to content

Add runnerVariants: many runner pod specs behind one listener and one RBAC bundle - #4614

Open
HarikrishnanBalagopal wants to merge 4 commits into
actions:masterfrom
HarikrishnanBalagopal:hb/runner-variants
Open

Add runnerVariants: many runner pod specs behind one listener and one RBAC bundle#4614
HarikrishnanBalagopal wants to merge 4 commits into
actions:masterfrom
HarikrishnanBalagopal:hb/runner-variants

Conversation

@HarikrishnanBalagopal

Copy link
Copy Markdown

What this does

Adds an optional runnerVariants list to AutoscalingRunnerSet. One scale set can now bring up runner pods of different specs (image, resources) behind a single listener pod and a single RBAC bundle (ServiceAccount, Role, RoleBinding, config Secret).

Fixes / addresses #4169.

When runnerVariants is empty the scale set behaves exactly as before, byte-for-byte. This is guarded by golden-hash tests.

Why

Today each shape of runner needs its own AutoscalingRunnerSet, and each ARS forces its own always-on listener pod plus its own RBAC bundle. On large clusters this multiplies pods and objects for no functional reason. #4169 reports a fleet of ~88 ARS that differ almost only in image and a small number of resource shapes. Splitting one pod spec per scale set id is a GitHub Actions protocol requirement; the number of listener pods and RBAC bundles is an ARC modeling choice.

github.com/actions/scaleset (v0.4.0) already exposes a per-scale-set-id, goroutine-safe MessageSessionClient, so one listener process can drive many scale sets. The whole feature therefore ships from this repo alone, with no scaleset release dependency.

Design

An ADR is included at docs/adrs/2026-08-28-runner-variants.md. In short:

  • Each variant carries a required DNS-label name, its own runnerScaleSetLabels, an optional pod template, and optional minRunners/maxRunners that override the ARS-level values.
  • When variants are set, one ARS registers one scale set id per variant, creates one EphemeralRunnerSet per variant, and creates exactly one AutoscalingListener.
  • The per-variant scale set tuples travel to the listener on an out-of-band actions.github.com/listener-scale-sets annotation, not in the hashed listener spec. Adding a slice field to the hashed spec would change the hash of every existing single-variant listener (spew prints nil slices and the type name) and force a fleet-wide listener pod recreation on upgrade. The annotation is absent for single-variant sets, so their listener spec, Role, and config.json stay byte-for-byte identical.
  • The listener process starts one supervised session per scale set. A transient error in one session is retried under capped backoff and does not cancel its siblings; only context cancellation or an unrecoverable auth/config error is fatal.

Commit layout (please review in order)

  1. Listener multi-session support (cmd/ghalistener). Optional scaleSets config; empty list keeps the original single-session behavior.
  2. CRD types + codegen + golden-hash back-compat tests (types unused by the reconciler yet, so the empty path stays byte-identical).
  3. Reconciler + resource builder fan-out, RBAC scoping over all child ephemeral runner sets, the runner-scale-set-ids annotation map, and the add / remove / delete / orphan flows. Includes a real fix: cleanupEphemeralRunnerSet used to look up only the ERS named after the ARS and would have orphaned variant ERS on delete; it now lists by label and deletes all owned sets.
  4. Helm chart + ADR + docs. Optional runnerVariants: values example, a template block, a helper that validates variant names, and chart template tests.

Ordering note: the reconciler must not emit scaleSets to a listener image that predates commit 1. Within one PR / release that is automatic.

Back-compat guarantee

For the empty-variant path these are pinned identical to the release tip by golden tests: the ephemeral runner set integrity hash, the listener spec hash, the Role integrity hash, and the runner-set spec hash. The ARS-level Hash() and ListenerSpecHash() change once (the spec gained a field), which is a single Pending-to-Running flip on the ARS on upgrade with no child object churn.

Testing

  • make generate manifests clean (no stray x-kubernetes-list-* keys under the new subtree).
  • gofmt, go vet, go build ./... clean.
  • Golden-hash unit tests green (BackCompat, ARSLevelHash, EffectiveVariants).
  • New reconciler table tests and a multi-variant envtest suite green (create: one ERS per variant with per-variant image and id, a two-entry id map, one listener with a two-tuple annotation, exactly one AutoscalingListener; remove-variant orphan delete; delete-ARS full cleanup).
  • Full controller envtest suite green (ok ... 181s, no regression).
  • cmd/ghalistener/... green under -race.
  • Chart template tests green (two-variant render fields; duplicate-name failure).

kind e2e / acceptance is not included in this PR; happy to add it if you would prefer it in-tree.

Note

#4169 does not have a maintainer response yet and CONTRIBUTING asks to discuss significant changes first. The ADR and this PR are the concrete proposal for that discussion. Merge is entirely at the maintainers' discretion; the design is written to keep the existing single-variant path unchanged so the change is safe to defer or revise.

The listener process is hard-wired to one scale set: it opens one message
session for one RunnerScaleSetID and drives one EphemeralRunnerSet. This is
the first part of the runnerVariants feature, which lets one AutoscalingRunnerSet
declare several runner variants served by one listener pod.

config: add an optional ScaleSets list to the listener config. When empty the
listener behaves exactly as before (single set from the scalar fields).
EffectiveScaleSets() returns one synthetic entry from the scalars in that case,
so the driver treats both modes the same way. validateScaleSets() checks each
entry and rejects duplicate EphemeralRunnerSet names.

metrics: split the exporter into a shared server (registry + HTTP endpoint) and
per-scale-set recorders. Each recorder stamps its own scale set name, so several
variants can export on one metrics port without colliding. The single-set path
keeps the same series and labels.

main: fan out one session per effective scale set. Single-set mode keeps the
original semantics exactly (a listener error propagates and the pod exits so the
controller recreates it). Multi-set mode supervises each session with capped
backoff so one bad session cannot cancel its siblings; only shutdown ends the run.

Tests cover EffectiveScaleSets, list validation, per-scale-set recorders, and the
supervisor restart + failure-isolation behaviour.
Add the RunnerVariant type and the optional RunnerVariants list to
AutoscalingRunnerSetSpec, plus the ListenerScaleSet tuple type and a
runner-variant label and runner-scale-set-ids annotation constant. The
types are not wired into the reconciler yet; this commit only introduces
the API surface and its generated code so the empty (single-shape) path
stays provably byte-for-byte identical.

EffectiveVariants resolves the spec into the variants to reconcile,
returning one default variant (empty name) carrying the top level values
when RunnerVariants is empty, so later commits can loop over the result
without a separate default code path.

Back-compat is guarded by golden hash tests captured from the
pre-runnerVariants HEAD: the EphemeralRunnerSet integrity hash, the
AutoscalingListener spec hash, the listener Role hash and the
RunnerSetSpecHash are all unchanged for the default path. The
AutoscalingRunnerSet level Hash and ListenerSpecHash change once (spew
includes the new nil field); that flips the set through Pending on the
first reconcile after upgrade but churns no child object, and the
post-change values are pinned so the one-time nature stays visible.

The Makefile strips the same x-kubernetes-list keys under
runnerVariants[].template.spec as it already does for the top level
template.spec; the two CRD subtrees are byte-identical after the fix.
Wire the reconciler and resource builder to the runnerVariants CRD field
added in the previous commit. When an AutoscalingRunnerSet declares one or
more variants it takes a fan-out path: one EphemeralRunnerSet per variant
behind a single AutoscalingListener and one RBAC bundle. A set with no
variants keeps the classic single-set path byte-for-byte, pinned by the
golden hash tests.

Reconciler (autoscalingrunnerset_controller.go):
- register a runner scale set id per variant; keep the scalar
  runner-scale-set-id annotation and add a variant name to id map on
  actions.github.com/runner-scale-set-ids, so single-set stays unchanged
- shouldCreateScaleSet re-enters registration when any declared variant
  still lacks an id, so adding a variant registers it incrementally
- reconcileMultiVariant creates the missing EphemeralRunnerSet per variant,
  patches drifted ones by integrity hash, and deletes orphans left by a
  removed variant
- reconcileMultiVariantListener keeps exactly one listener, built from the
  first variant's EphemeralRunnerSet, carrying the scale set tuples on the
  out of band actions.github.com/listener-scale-sets annotation so the
  listener spec and hash stay stable
- run the multi-variant branch before the scalar runner group and name
  reconciliation, which are single-scale-set concepts
- cleanupEphemeralRunnerSet lists owned sets by label so no variant is
  orphaned on delete; the single set matches the same labels

Resource builder (resourcebuilder.go):
- newEphemeralRunnerSetForVariant names the set, resolves its scale set id,
  applies the variant template, and stamps the runner-variant label only
  for a named variant
- the listener Role scopes over every child EphemeralRunnerSet name decoded
  from the listener annotation; single-set scopes over the one scalar name
- newScaleSetListenerConfig appends the extra scale sets to config.json only
  when the annotation is present, so single-set config bytes are unchanged

Tests: table tests for the pure helpers (naming, id resolution, annotation
round trips, shouldCreateScaleSet, isMultiVariant) and an envtest suite that
creates a two variant set and asserts one EphemeralRunnerSet per variant with
its own id and template, a single listener carrying both scale sets, orphan
cleanup on variant removal, and full cleanup on delete.
Render an optional runnerVariants list on the AutoscalingRunnerSet chart,
with a values example and a helper that rejects duplicate or non-DNS-label
variant names and names that would make the EphemeralRunnerSet name longer
than 63 characters. Add template tests for the two-variant render and the
duplicate-name failure, and an ADR that records the design and its
back-compat guarantees.
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