Skip to content

feat: bootstrap new instances in-process instead of with Jobs#11231

Draft
armru wants to merge 11 commits into
mainfrom
dev/remove-jobs
Draft

feat: bootstrap new instances in-process instead of with Jobs#11231
armru wants to merge 11 commits into
mainfrom
dev/remove-jobs

Conversation

@armru

@armru armru commented Jul 20, 2026

Copy link
Copy Markdown
Member

Bootstrapping an instance today goes through a dedicated batch/v1 Job per mode (<instance>-initdb, -join, -pgbasebackup, -full-recovery, -snapshot-recovery): the operator creates the PVCs, waits for the Job to initialize them, marks them ready, and only then creates the instance Pod. Every bootstrap therefore pays two pod schedules and image pulls, reconciliation stays blocked while the Job runs, and the operator has to coordinate two consumers of the same PVCs.

With this change the operator creates the instance Pod directly. When the pgdata PVC is not marked ready yet, the Pod carries a bootstrap overlay: instance run receives --bootstrap-mode=<mode> plus exactly the flags the Job builders used to emit, and the Pod is annotated with cnpg.io/bootstrapInstance for observability. Before starting the instance manager, instance run initializes PGDATA in-process with the same library code the instance <mode> subcommands are built on, while a minimal webserver keeps the kubelet probes and wal-restore working: the startup probe is answered during the whole data copy instead of timing out, readiness stays negative, and /pg/status answers 503 so the operator sees the instance as not reporting yet. The overlay is deliberately left out of the pod-template annotation, so a freshly bootstrapped Pod is not considered drifted and no rollout is triggered.

Completion is tracked at two levels. A marker file inside PGDATA records the mode and the instance identity (namespace, cluster name and UID, pod name): a container restarting mid-bootstrap finds no marker and retries the mode from scratch (except snapshot recovery, which resumes in place, since a snapshot-seeded PGDATA cannot be re-obtained), while a marker that traveled inside a volume snapshot belongs to a different identity and is ignored, so a snapshot-cloned PVC is bootstrapped properly instead of being mistaken for an already-initialized one. The cnpg.io/pvcStatus annotation remains the operator-side memory, and now flips to ready only when the owning Pod becomes Ready.

User-visible effects: no bootstrap Jobs are created anymore (the pg_upgrade Job is unchanged); bootstrap progress and failures surface on the instance Pod itself, in its logs and restart count; and since the Pod's hostname is the instance name from the start (the Jobs used the Job name), application_name and everything else derived from POD_NAME is correct during the initial data copy too. Recovery sources protected by a private CA no longer rely on a Secret mounted over /controller/certificates: the bootstrap writes the CA file itself, which also removes a mount that would have shadowed the directory where the instance manager maintains the server certificates.

Operator upgrades need no migration: the Job read/guard/cleanup paths are all kept, so an in-flight legacy bootstrap Job still blocks reconciliation, still marks its PVC ready on completion, and still surfaces its failure; from then on those paths only ever see the pg_upgrade Job.

Recovery through a CNPG-I plugin now runs inside the instance Pod, so plugins must serve the restore protocol from their instance sidecar (there is no restore Job carrying a dedicated sidecar anymore): cloudnative-pg/plugin-barman-cloud#1025 is the companion change. Removing the now-unused Job builders and the five bootstrap subcommands, and reworking how a failed bootstrap reaches PhaseUnrecoverable without a Job backoffLimit, are left to follow-ups; the failure policy is the open question tracked on the issue.

The e2e bootstrap assertions are retargeted from Jobs to Pods and now also verify that no bootstrap Job is ever created; a walStorage variant covers the separate WAL volume layout.

Closes #11228

@armru
armru requested review from a team, NiccoloFei, jsilvela and litaocdl as code owners July 20, 2026 16:30
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. do not backport This PR must not be backported - it will be in the next minor release enhancement 🪄 New feature or request labels Jul 20, 2026
@cnpg-bot cnpg-bot added backport-requested ◀️ This pull request should be backported to all supported releases release-1.28 release-1.29 release-1.30 labels Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

❗ By default, the pull request is configured to backport to all release branches.

  • To stop backporting this pr, remove the label: backport-requested ◀️ or add the label 'do not backport'
  • To stop backporting this pr to a certain release branch, remove the specific branch label: release-x.y

@armru
armru marked this pull request as draft July 20, 2026 16:39
@armru

armru commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

/test barman_plugin=pr-1025

@github-actions

Copy link
Copy Markdown
Contributor

@armru, here's the link to the E2E on CNPG workflow run: https://github.com/cloudnative-pg/cloudnative-pg/actions/runs/29762494135

@armru

armru commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

/test barman_plugin=pr-1025

@github-actions

Copy link
Copy Markdown
Contributor

@armru, here's the link to the E2E on CNPG workflow run: https://github.com/cloudnative-pg/cloudnative-pg/actions/runs/29812087177

armru added 8 commits July 21, 2026 17:36
The five bootstrap subcommands (initdb, join, pgbasebackup, restore,
restoresnapshot) each carried their PGDATA initialization logic inline in
their cobra command, which made it impossible to run a bootstrap without
spawning the matching subcommand in a dedicated Job. Move that logic into a
new pkg/management/postgres/bootstrap package exposing an Instruction type
and a single Execute dispatch, and reduce the subcommands to flag parsing
that delegates to it. The per-mode behavior is preserved exactly, including
that only restoresnapshot skips the target-directory cleanup.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
Add a per-instance bootstrapProgress flag carrying the bootstrap mode and
start time, so the instance manager can advertise that PostgreSQL is not up
yet while it initializes the data directory in-process. While the flag is set
the startup probe is skipped (the copy phase has no socket and can outlast the
probe budget), /pg/status replies with a 503 and a recognizable body instead
of a valid payload (a valid one would make the operator short-circuit its
reconciliation guards on an unready pod), and an instance-manager update is
refused with a 409. The flag is intentionally separate from mightBeUnavailable
so a bootstrapping pod is never counted as reporting its status, which would
break primary-before-replica ordering.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
Teach instance run to initialize an uninitialized PGDATA in-process before the
controller-runtime manager starts, so a data directory can be bootstrapped
without a dedicated Job. When run is given a --bootstrap-mode (plus the same
per-mode flags the Job builders used to emit) and the data directory has no
completion marker yet, run flips the instance bootstrap flag, serves a minimal
status server and the local web server, dispatches the bootstrap through the
shared library, and on success writes a durable completion marker inside PGDATA
before falling through to the normal run. The manager cannot start earlier
because its in-pod reconciler would write into an empty PGDATA, and the marker
makes a container restart after a successful bootstrap skip straight to the
normal run. The marker is fsynced together with its parent directory because
WriteFileAtomic only flushes the file contents, not the rename.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
Prepare the operator to bootstrap an instance by turning its own pod into a
bootstrapping pod instead of creating a dedicated Job. The per-mode flag lists
that the six bootstrap Job builders assembled inline are extracted into shared
arg builders, and a new ApplyBootstrapOverlay stamps those flags (prefixed with
--bootstrap-mode) plus the volumes, mounts, environment and annotation each
mode needs onto a steady-state instance pod produced by NewInstance. The
overlay leaves the podSpec and podEnvHash annotations alone so a
just-bootstrapped pod is not mistaken for a drifted one. Nothing calls the
overlay yet, so the Job builders keep working exactly as before.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
The five bootstrap paths (initdb, join, pgbasebackup, recovery and
volume-snapshot restore) no longer run in a dedicated batch Job before the
instance Pod is created. The operator now builds the steady-state instance Pod
and applies the bootstrap overlay to it, so the Pod initializes its own data
directory in-process and then starts PostgreSQL. Primary creation, replica join
and reattachment go through a single shared builder, and the lost-Job recovery
that guarded #11036 becomes a plain Pod recreation for any serial whose data PVC
never finished bootstrapping, covering both the interrupted first primary and a
bootstrapping Pod evicted before it completed.

A data PVC is now flipped to ready only once its owning Pod reports ready,
instead of as soon as any Pod attaches, so a Pod still bootstrapping in-process
no longer marks its not-yet-initialized volume as ready and cannot be mistaken
for a fully bootstrapped one on the reattach path. The pg_upgrade Job and every
Job read, guard and cleanup path are left untouched.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
…of mounting it

The bootstrap overlay used to mount the barman endpoint CA Secret on the
instance's certificates directory and point AWS_CA_BUNDLE at it. That was
harmless for the short-lived bootstrap Job, but the job-free bootstrap keeps the
same pod running as the instance, and a read-only Secret mount over that
directory shadows the place where the instance manager writes its server
certificate: the certificate reconciler then fails with a read-only filesystem
error and the pod never becomes ready.

Drop the CA mount from the overlay and instead write the recovery-source endpoint
CA to disk during the in-process bootstrap, before barman-cloud runs, so the CA
files are owned by the instance manager exactly as on every other pod. The write
reuses the certificate reconciler's existing secret-to-file helper and resolves
the source CA with the same precedence the Job builders used: the recovery backup
reference, then the origin backup status, then the recovery source external
cluster. The container-level AWS_CA_BUNDLE the Job injected is not restored, as
the in-process restore and wal-restore build their own environment and only need
the CA file present at the unchanged location.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
…identity

The in-process phase-0 bootstrap writes a completion marker inside PGDATA
so a container restart can skip work that already finished. Because the
marker lives inside PGDATA, it travels inside volume snapshots: a PVC
cloned from a snapshot already carries the source instance's marker, so
IsCompleted reported the data directory as already bootstrapped and
runBootstrap skipped the whole restoresnapshot phase-0. That lost the PITR
recovery configuration (the cluster promoted from the snapshot data without
fetching the archived WAL up to the requested target) and left snapshot
cloned scale-up replicas without their demotion and replica configuration.

Scope the marker to the instance that wrote it. The marker payload now
records the namespace, cluster name, cluster UID and pod name, and
IsCompleted reports completed only when the marker exists, parses, and
matches the current instance. A marker inherited from another instance
(or an unparseable or old-format one) is treated as not completed, so
phase-0 runs and rewrites the marker with the new identity. The cluster is
now loaded before the marker check to provide the cluster UID.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
Issue #11228 replaces every initdb/join/pgbasebackup/restore/restoresnapshot
bootstrap Job with a self-bootstrapping instance Pod, so e2e assertions that
checked batch/v1 Job objects for these paths no longer describe what the
operator does. The microservice import-failure test now watches the
bootstrap pod's restart count instead of a Job's failed count (the exact
tier of the failure-surfacing policy is still open on #11228, so the
assertion only relies on the pod restarting rather than the threshold); the
#10985 regression test now checks that no new instance Pod appears for the
recreated serial while the previous WAL PVC is terminating, instead of
checking for the absence of a join Job. The initdb, join, pgbasebackup, and
barman/volume-snapshot recovery suites (in-tree and via plugin-barman-cloud)
gained a shared assertion that no bootstrap Job is ever created while a
cluster is provisioned, plus a check that the resulting instance pods reach
Ready without restarting their postgres container.

A small tests/utils/jobs package and two new assertions in
tests/internal/asserts/cluster (AssertNoBootstrapJobCreatedDuring,
AssertClusterInstancesHaveNoRestart) back these checks so they can be reused
across suites instead of inlining the same polling logic everywhere.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
armru added 3 commits July 21, 2026 17:36
The e2e list for #11228 had no walStorage variant: the bootstrap Job used to
lay out pg_wal before run's ReconcileWalDirectory executed, and the in-process
replacement needs the same ordering verified with a dedicated WAL volume
configured. The existing dedicated-WAL-volume test already creates a
3-instance cluster with walStorage, exercising both initdb and join, so it is
extended with the same no-bootstrap-Job and no-restart assertions used by the
other bootstrap suites rather than adding a near-duplicate spec.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
…t CA secret

Phase-0 bootstrap now reads the recovery endpoint CA through the
Kubernetes API instead of the bootstrap Job's Secret volume mount, so the
instance Role must list that secret among the ones it can get. The Role
already enumerated the origin Backup credentials and the external cluster
endpoint CA, but not the endpoint CA coming from the origin Backup status
or from a recovery Backup reference in the cluster spec. Without those
entries every restore from a Backup reference against a TLS-protected
object store crashloops on a forbidden Secret get.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
With in-process bootstrap the operator reuses the freed instance serial
and recreates the pod with the same name within seconds, so the old
primary pod name never stays absent long enough for the test to observe
it. Capture the old primary pod UID before deletion and assert the pod is
either gone or has been replaced by a new one carrying a different UID,
keeping the existing timeout.

Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-requested ◀️ This pull request should be backported to all supported releases do not backport This PR must not be backported - it will be in the next minor release enhancement 🪄 New feature or request ok to merge 👌 This PR can be merged size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Bootstrap PostgreSQL instances without a separate Job or init container

3 participants