Skip to content

Cell Pools

A CellPool is a cluster-scoped custom resource. It holds the size, the capacity, and the template of one pool of cells that serve the pipelines tenants of a single version profile. The operator sizes each pool from its own spec on a 60-second ticker, and reports the result in status.

Read Cells and pipelines tenants first.

FieldTypeMeaning
profileRefstring, required, immutablethe JenkinsVersionProfile the pool’s cells run
warmint32, default 0target count of free cells the pool keeps ready
maxCellsint32, default 0cap on the pool’s cells, placed cells included. 0 means unlimited
maxTenantsPerCellint32, minimum 1, default 10capacity a pool cell uses when its own spec.cell.maxTenants is zero
maxRunsPerTenantint32, 0 to 10000, default 0builds one tenant runs at once on one cell; zero means no cap. A build over the cap waits. Set it on the object; the scale API does not change it
unreadyGracePeriodduration, optionalhow long a pool cell may stay unready before the operator rolls its pod
cellTemplateobject, optionalresources, miteResources, and podOverrides for the cells the pool creates
librarieslist, optionaltrusted shared libraries that every cell of the pool serves. See Pool shared libraries
approvedSignatureslist of strings, optionalscript-security signatures that every cell of the pool approves. See Script approvals
mailobject, optionalSMTP server for the mail and emailext steps of every cell of the pool. See Slack and email
giteaServerslist, optional, max 16Gitea and Forgejo servers every cell of the pool declares. See Commit status

profileRef is immutable. A pool’s cells carry the profile label they were created with, and a change would orphan every one of them. To serve another profile, create another pool.

FieldMeaning
warmCellsowned cells that hold no tenant
placedCellsowned cells that hold at least one tenant
unreadyCellsowned cells whose CellReady condition is not True
freeTenantSlotsspare capacity of the placed cells only
tenantsevery tenant on the pool, sorted

PoolReady reasons:

ReasonStatusHolds whenAction
ProfileMissingFalseprofileRef names no JenkinsVersionProfileCreate the JenkinsVersionProfile, or fix profileRef.
RecordStoreUnsupportedFalserecordStore.enabled is falseEnable the record store.
BlobTierUnsupportedFalserecordStore.blobs.mode is not s3Set the blob tier to s3.
ArtifactSecretMissingFalseno artifact credential is configured, or <artifactSecret>-<pool> (STS mode: the issuer Secret) does not exist with dataCreate the Secret the message names.
ArtifactPluginMissingFalsethe plugin set a cell of the pool’s version resolves does not pin artifact-manager-s3Pin artifact-manager-s3 in the version profile.
SecretsMissingFalsethe pool holds no usable confidential storeRead Pool secrets below.
CellCapReachedFalsemaxCells is above zero and the pool owns that many cellsRaise maxCells.
ReadyTruethe profile exists, the pool is below its cap, and no fill precondition refused a fillNone.

The first four precondition reasons report only on a pass that wanted a new cell. A pool at its target therefore reports CellCapReached or Ready, even with a missing artifact Secret. SecretsMissing is the exception: the store is a property of the pool, not of a fill.

Reasons rank from the top row down. The status reports the highest that applies.

The operator creates CellPool/<profile> for each profile a pipelines Team requests, when cells.warmPool.enabled is true. The chart values seed the new pool:

cells:
warmPool:
enabled: true
perProfile: 1
maxTenantsPerCell: 10
unreadyGracePeriod: 5m

Each value seeds a new pool only. A Helm upgrade never rewrites the spec of an existing pool.

The chart refuses a cells.maxTenantsPerCell or cells.warmPool.perProfile below 1.

To create a second pool, apply one:

apiVersion: varroa.dev/v1alpha1
kind: CellPool
metadata:
name: solo
spec:
profileRef: jenkins-lts
warm: 1
maxTenantsPerCell: 1

Creation and deletion are kubectl or GitOps operations. The API and the CLI only read and scale a pool.

cells.cellTemplate seeds the cellTemplate of each pool the operator creates:

KeyDefault
resourcesrequests cpu: 1, memory: 3Gi; limit memory: 6Gi
miteResourcesrequests cpu: 10m, memory: 32Mi; limit memory: 128Mi
podOverrides.jvmOpts-XX:MaxRAMPercentage=50
podOverrides.affinitypreferred pod anti-affinity on kubernetes.io/hostname

The heap takes half the memory limit. The anti-affinity spreads the pods of all pools across nodes, and a small cluster still schedules them. Values merge into the default. Set a key to null to remove it:

cells:
cellTemplate:
resources:
requests: {cpu: "2", memory: 6Gi}
limits: {memory: 12Gi}
podOverrides:
affinity: null

Set cells.cellTemplate: null for pools with no template. A pool that exists keeps its own template. Edit the pool to change it, as Template edits describes.

Each cluster of the brood holds its own pools. A pool name is unique inside one cluster only.

Terminal window
varroactl get cellpools
varroactl describe cellpool jenkins-lts
varroactl scale cellpool jenkins-lts --warm 3 --max-cells 8
varroactl scale cellpool lts-2-570 --cluster east --warm 2

get cellpools lists every cluster, with a CLUSTER column. A command on one pool uses the core cluster when you give no --cluster.

GET /api/v1/cellpools adds a cluster field to each pool and a clusters array with one read status for each cluster. A cluster that does not answer has ok: false. GET and PATCH /api/v1/cellpools/{name} take ?cluster=<name>, and an unreachable member gives 502. A member operator on an older image does not answer until you upgrade it.

The same edit through kubectl:

Terminal window
kubectl patch cellpool jenkins-lts --type=merge -p '{"spec":{"warm":3}}'

A watch wakes the operator, so a scale edit takes effect within seconds rather than after the 60-second backstop.

Lowering warm drains free cells only. The pool never deletes a cell that holds a tenant, nor one a tenant is being placed on. Such a cell leaves the pool one above its target until the next pass. Deleting a pool drains its free cells. A placed cell keeps every label and lives on as an ordinary cell, resolving its capacity from its own spec.cell.maxTenants or from cells.maxTenantsPerCell.

A fill needs the install-wide gates of the PoolReady table. In static mode it also needs <artifactSecret>-<pool name>. In STS mode it needs no Secret per pool. See Artifact credentials.

A pool with warm: 0 holds no cell until a tenant waits for placement, and the pass then creates one. The tenant reports Placed=False with reason AwaitingCell while that cell boots, usually one to three minutes. Set warm to 1 or more to remove that wait.

The pool re-stamps spec.version, spec.resources, spec.podOverrides, and spec.miteSpec.resources on each free cell every tick. It keeps the other spec.miteSpec fields, and never edits a cell that holds a tenant. A cellTemplate edit therefore rolls the free cells at once, and reaches a placed cell only after it drains. To grow a placed tenant, move it to a pool with a larger template.

A cell keeps $JENKINS_HOME on an emptyDir, so each instance would mint its own secrets/ directory and reject the last one’s agents. Each pool therefore owns one Jenkins confidential store: the Secret varroa-cell-secrets-<pool> in the operator namespace, labelled varroa.dev/cell-pool: <pool>. Every cell of the pool starts with it, and each key name is also the file name Jenkins reads.

KeyProtects
master.keyevery other key of the store
jenkins.slaves.JnlpSlaveAgentProtocol.secreteach inbound agent’s own secret
hudson.util.Secretevery Secret value in a config file
org.jenkinsci.main.modules.instance_identity.InstanceIdentity.KEYthe instance identity

The operator mints the store once for each pool, and never rotates it. It never overwrites a key that holds a value, and builds a missing key under the existing master.key. A repaired key reaches only the cells that start after the repair, so deleting a key is never a rotation path.

Two states never repair themselves: a store with no master.key, and a key that does not decrypt under it. The pass then writes nothing and reports PoolReady=False with SecretsMissing. Delete the Secret and roll the pool to recover. A pool cell that finds no usable store does not start: its reconcile blocks with ReconcileBlocked=True and the reason CellSecretsFailed.

Blast radius. Every cell of a pool accepts every agent secret that pool minted, which is the point of the shared store. Set maxTenantsPerCell: 1 to narrow the radius to one tenant. The operator holds each pool’s master key, beside every bootstrap Secret and the mite CA.

Rotate a pool store by hand.

  1. Delete the Secret: kubectl delete secret varroa-cell-secrets-<pool> -n varroa-system.
  2. Wait for the next pool tick, and confirm PoolReady reports Ready.
  3. Roll every cell: kubectl delete pod -n varroa-system -l varroa.dev/cell-pool=<pool>.

The roll drops every running agent. Every value encrypted under the old hudson.util.Secret becomes unreadable, so re-apply those credentials.

A cell carries three labels. varroa.dev/cell-pool names its pool, varroa.dev/cell-profile names its profile, and varroa.dev/warm records that a pool created it. A pool owns a cell only while the pool its label names still exists.

A pool cell’s pod also carries varroa.dev/cell-pool, set when the operator builds the pod. A running cell gets it at its next roll. A podOverrides.podLabels entry cannot change it.

A pool whose name equals its own profileRef is that profile’s default pool. It adopts every cell of its profile that carries no pool label. A pool with any other name adopts nothing.

The cellpools resource carries two verbs. read lists and gets a pool; update scales one. The built-in varroa:operator role holds both, and varroa:admin holds both through its wildcard. varroa:developer and varroa:viewer hold neither. See Varroa RBAC.

Verify:

Terminal window
kubectl get cellpool
kubectl get controller -n varroa-system -l varroa.dev/warm=true

Expect one Controller per warm count for each pool, each reporting WarmPool=True.