mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 22:13:18 +09:00
## What this PR does Fixes #3322. An in-place Postgres (CNPG) restore decided whether to skip deleting the live `Cluster` by comparing its `creationTimestamp` with the RestoreJob's `StartedAt`. The timestamp says the Cluster is new, not which restore rendered it, so two cases let a restore skip its purge and report `Succeeded` on data from another restore: 1. Two RestoreJobs into the same application at once. The second sees the Cluster the first produced, created after its own `StartedAt`, and skips its purge. 2. The live Cluster is deleted while the restore waits for the WAL archive. The still-active HelmRelease re-renders it from the previous restore's values, again created after `StartedAt`. ### Refuse a restore into an application another restore holds The RestoreJob controller now refuses, for every driver, a RestoreJob that has not started yet when another unfinished RestoreJob in the namespace writes into the same application and goes first. The refused job fails with reason `ConcurrentRestore`, and its message names the job that goes first. It fails instead of waiting, so the application is never restored a second time later, after the first restore, by a job its author may no longer expect to run. A started restore goes before one that has not started. Between two that have not, the older goes first, by creation time and then by name; creation times have one-second precision, so two jobs created in the same second are ordered by name. A running restore is never failed by a newer one. The target application is resolved by `restoreJobTarget`, the same helper the BackupJob hold uses, so a Velero restore into another namespace does not collide with an in-place one. The gate lists RestoreJobs through the manager's API reader, not the informer cache. A job reconciled before the cache holds its peer's `StartedAt` would otherwise see that peer as not started and, on a same-second name tie, proceed alongside it. The controller runs a single worker, so a peer's `StartedAt` written by an earlier reconcile is already on the API server when the next job reads it; `SetupWithManager` now sets `MaxConcurrentReconciles: 1` explicitly, since the guarantee depends on it. ### Skip the CNPG purge only for this restore's Cluster The restore patch already sets `bootstrap.newServerName` to `restoredServerName(cluster, RestoreJob UID)`, a value unique to each RestoreJob. The postgres chart now annotates the `Cluster` with it as `postgres.cozystack.io/restored-server-name`. The annotation is rendered even when the barman plugin is not; the plugin's `serverName` also carries the value, but the plugin renders only with chart-level S3 coordinates. The driver skips the purge only when the annotation names this RestoreJob. A Cluster produced by a concurrent restore, or re-rendered from an earlier restore's values, carries another value and is purged. The mark lives on the rendered Cluster rather than in RestoreJob status. That keeps it valid in the case the skip exists for: the purge succeeded, but the `TargetPurged` status write failed. A UID recorded in status was rejected in the code for exactly that reason. A Cluster rendered by a chart that predates the annotation has none and keeps the creation-time check, so a restore in flight across an upgrade behaves as before. ### Tests - `TestRestoreAhead` covers order, a started job going first, the name tie-break, finished and terminating jobs, other applications, retargeted restores and a Velero restore into another namespace. - `TestReconcileRestore_ConcurrentRestoreIsRefused` checks that the newer job fails with `ConcurrentRestore` and the running one is left alone. - `TestReconcileRestore_ConcurrentRestoreReadsPastTheCache` gives the cache a stale, not-started peer and the API reader the started one, and requires the job to fail with `ConcurrentRestore`. - `TestCNPGClusterFreshlyRecovered` adds both cases from the issue: a Cluster marked by another restore and created after `StartedAt` must be purged. The timestamp-only cases are kept for the unmarked fallback. - `TestReconcileCNPGRestore_RepeatInPlacePurgesStaleRecoveryCluster` gains two reconcile-level cases. One purges a Cluster that another restore marked, even though it was created after `StartedAt`. The other runs a full purge, takes the mark from the `bootstrap.newServerName` the restore patch wrote, drops `TargetPurged` as a lost status write would, and requires a Cluster carrying that mark to survive, although it predates `StartedAt`. That pins the purge decision to the value the chart actually renders. - `packages/apps/postgres/tests/restore_mark_test.yaml` checks that the annotation is rendered with and without the barman plugin, and is absent on a Cluster that was never restored. Each new test was checked against the code it guards: disabling the gate call, listing through the cache, flipping the started-job precedence, ignoring the annotation, or deriving the expected mark from the app name instead of the cluster name each turns one red. `go test ./internal/backupcontroller/...` and `helm unittest packages/apps/postgres` (152 tests) pass. The restore flow was not run against a live cluster. `docs/operations/backup-classes.md` documents the one-restore-at-a-time rule and the annotation. ### Screenshots N/A — no UI changes. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` against the diff: Go changes in `internal/backupcontroller` (no CRD or API type change), one annotation added to the postgres chart's Cluster (no `values.yaml` or `values.schema.json` change), and a docs paragraph. No package is added or renamed, no platform values, ApplicationDefinition, RestoreJob CRD, metric, label or namespace changes, so no trigger is reached. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note fix(backups): a RestoreJob now fails with reason ConcurrentRestore when another unfinished RestoreJob is already restoring the same application, instead of running alongside it; create it again once the first restore has finished. An in-place Postgres restore now skips deleting the live Cluster only when that Cluster was rendered for this RestoreJob, identified by the new postgres.cozystack.io/restored-server-name annotation, so it no longer reports Succeeded on data from a concurrent or earlier restore. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Restores targeting the same application are now handled one at a time. A competing restore is refused rather than queued, with started restores taking priority. * PostgreSQL now calculates a default `shared_buffers` value when the memory limit is below 512 MiB, while respecting explicitly configured values. * **Bug Fixes** * Recovery clusters are more reliably distinguished from manual recoveries and clusters created by other restores, reducing the risk of purging the wrong cluster. * **Documentation** * Clarified restore ordering and how recovery-cluster cleanup is determined. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
480 lines
26 KiB
YAML
480 lines
26 KiB
YAML
{{- $tlsEnabled := (include "postgres.tls.enabled" .) | eq "true" -}}
|
|
{{- $tlsRequired := (include "postgres.tls.required" .) | eq "true" -}}
|
|
{{- $autoscaling := (include "postgres.autoscaling.active" .) | eq "true" -}}
|
|
---
|
|
apiVersion: postgresql.cnpg.io/v1
|
|
kind: Cluster
|
|
metadata:
|
|
name: {{ .Release.Name }}
|
|
{{- with .Values.bootstrap.newServerName }}
|
|
{{- /* Rendered whether or not the barman plugin is: the CNPG restore driver
|
|
reads it to tell the Cluster its own restore produced from one rendered
|
|
for an earlier restore. */}}
|
|
annotations:
|
|
postgres.cozystack.io/restored-server-name: {{ . | quote }}
|
|
{{- end }}
|
|
spec:
|
|
{{- if $autoscaling }}
|
|
{{- /* Under active autoscaling KEDA owns the runtime count via the /scale
|
|
subresource. The chart SEEDS spec.instances at a constant = max(replicas,
|
|
effectiveMin) (postgres.autoscaling.activeSeed) rather than omitting it, for
|
|
three reasons: (1) with synchronous quorum an omitted field defaults to 1 and
|
|
CNPG's webhook rejects "maxSyncReplicas must be lower than the number of
|
|
instances"; (2) omitting it on an EXISTING cluster's enable/upgrade makes
|
|
three-way merge delete the field, CNPG re-defaults to 1, and every replica +
|
|
its PVC is decommissioned before KEDA re-clones (hours with no read replicas);
|
|
(3) seeding effectiveMin ALONE differs from the else-branch (instances:
|
|
replicas), so a dryRun/transition flip would rebase a staged live count down to
|
|
the bare floor. Seeding max(replicas, effectiveMin) equals the else-branch value
|
|
whenever replicas is staged at/above the floor, so those flips render the same
|
|
value and the live count does not dip; it still never undercuts the quorum floor.
|
|
Because the seed is constant, each reconcile renders the value it rendered last
|
|
time, so the client-side patch is empty and never reverts KEDA's higher live
|
|
value. This holds ONLY under client-side apply, where helm-controller patches
|
|
this CRD from the previous-vs-new rendered manifest and does not consult live
|
|
state (it leaves Helm's threeWayMergeForUnstructured at the default): the
|
|
platform's helm-controller (v1.5.0) defaults to SERVER-SIDE
|
|
apply, which force-owns every rendered field and would revert spec.instances from
|
|
KEDA's live /scale value on every reconcile. (Editing replicas / minReplicas /
|
|
quorum.maxSyncReplicas moves the constant, so that one reconcile DOES patch
|
|
spec.instances to the new seed — a documented floor-change caveat, see values.yaml.)
|
|
The postgres ApplicationDefinition
|
|
therefore forces client-side apply via the annotation
|
|
release.cozystack.io/helm-server-side-apply: "false"
|
|
(packages/system/postgres-rd/cozyrds/postgres.yaml), threaded onto the tenant
|
|
HelmRelease's Install/Upgrade.ServerSideApply in
|
|
pkg/registry/apps/application/rest.go (convertApplicationToHelmRelease) and
|
|
guarded by TestConvertApplicationToHelmRelease_ServerSideApplyOverride.
|
|
(Go-template comment, stripped at render.) */}}
|
|
instances: {{ include "postgres.autoscaling.activeSeed" . }}
|
|
{{- else }}
|
|
{{- /* Static-count path (autoscaling off / dryRun / transition), which renders
|
|
instances: <replicas>. CNPG's webhook rejects the Cluster if maxSyncReplicas
|
|
>= instances; guard the DISABLE case in particular, where an autoscaled +
|
|
sync-quorum cluster runs with instances seeded from effectiveMin while
|
|
replicas may sit at its default, so a naive disable would render an invalid
|
|
count and wedge the HelmRelease. Fail closed with an actionable message.
|
|
(Stripped at render.) */}}
|
|
{{- if le (int .Values.replicas) (int .Values.quorum.maxSyncReplicas) }}
|
|
{{- fail (printf "postgres: replicas (%d) must be greater than quorum.maxSyncReplicas (%d) - CNPG rejects a Cluster whose maxSyncReplicas is not lower than its instance count. Raise replicas above maxSyncReplicas. (If you are disabling autoscaling on a synchronous-quorum cluster, first set replicas to at least the live instance count: kubectl scale cluster.postgresql.cnpg.io/%s --replicas=<live count>, with the HelmRelease suspended.)" (int .Values.replicas) (int .Values.quorum.maxSyncReplicas) .Release.Name) }}
|
|
{{- end }}
|
|
instances: {{ .Values.replicas }}
|
|
{{- end }}
|
|
{{- if and $tlsEnabled .Values.external }}
|
|
# Operator-managed TLS. CNPG auto-generates its own self-signed CA and signs
|
|
# server + client + replication certs from it. The operator's default SAN list
|
|
# already covers the three built-in services (rw/r/ro) in four DNS forms each
|
|
# (bare, .ns, .ns.svc, .ns.svc.<cluster-domain>); only the external hostnames
|
|
# need to be appended via serverAltDNSNames.
|
|
#
|
|
# serverAltDNSNames is mutually exclusive with serverTLSSecret per the CNPG
|
|
# admission webhook, which is why the chart no longer renders a cert-manager
|
|
# chain — the operator owns the chain end-to-end.
|
|
#
|
|
# Trust anchor for clients: the key-free "<release>.tenant-ca" Secret the CA
|
|
# extraction controller publishes, as declared by the sentinel in
|
|
# templates/tenant-projection.yaml. Tenants read that Secret through the
|
|
# core.cozystack.io tenantsecrets API. It is neither <release>-credentials,
|
|
# which is chart-rendered and holds user passwords only, nor CNPG's
|
|
# <release>-ca, which holds the CA private key next to ca.crt.
|
|
#
|
|
# The second name is the per-engine one, meant for SNI routing through a
|
|
# TLS-passthrough listener on the tenant Gateway (*.postgres.<apex>). Both
|
|
# are issued so that a later move between the two hostname schemes reissues
|
|
# no certificate and changes no client's trust anchor; clients still change
|
|
# the host they dial.
|
|
{{- $apex := include "cozy-lib.ns-host-required" . }}
|
|
certificates:
|
|
serverAltDNSNames:
|
|
- {{ printf "%s.%s" .Release.Name $apex }}
|
|
- {{ printf "%s.postgres.%s" .Release.Name $apex }}
|
|
{{- end }}
|
|
{{/*
|
|
s3Credentials points at the operator-supplied Secret when
|
|
backup.s3CredentialsSecret.name is set; otherwise it falls back to the
|
|
chart-managed <release>-s3-creds Secret materialised from s3AccessKey/
|
|
s3SecretKey. The override path keeps credentials out of the Postgres
|
|
CR .spec — the CNPG backup driver uses it on restore.
|
|
*/}}
|
|
{{- $credCfg := default (dict) .Values.backup.s3CredentialsSecret }}
|
|
{{- $credSecretName := $credCfg.name | default (printf "%s-s3-creds" .Release.Name) }}
|
|
{{/*
|
|
Platform-managed default flow (useSystemBucket=true):
|
|
- The chart does NOT render spec.plugins or the barman-cloud
|
|
ObjectStore. The backup-controller's CNPG driver SSA-applies a
|
|
barmancloud.cnpg.io ObjectStore (carrying destinationPath/
|
|
endpointURL/credentials) and SSA-patches the live Cluster's
|
|
spec.plugins to reference it at first BackupJob time, using field
|
|
manager "cozystack-cnpg-backup-driver" with ForceOwnership (see
|
|
internal/backupcontroller/cnpgstrategy_controller.go
|
|
applyClusterPluginBackup). The postgres HelmRelease is applied
|
|
CLIENT-SIDE (release.cozystack.io/helm-server-side-apply: "false",
|
|
for the autoscaler seed invariant), and Helm's client-side patch
|
|
only diffs fields the chart RENDERS (previous-vs-new manifest);
|
|
since the chart never renders spec.plugins under useSystemBucket, a
|
|
subsequent Flux re-render leaves the driver's SSA+ForceOwnership
|
|
patch intact — no tug-of-war on the live Cluster (the two apply on
|
|
disjoint field sets).
|
|
Coordinates come from a single backupStorage values block in
|
|
the backupstrategy-controller chart. This eliminates the
|
|
endpoint-duplication failure mode (an operator switching the
|
|
platform endpoint would otherwise have to update both the
|
|
backupstrategy-controller and this chart).
|
|
- Consequence: WAL archiving via the plugin is not active until the
|
|
first BackupJob has run. WAL accumulates on the local PVC in the
|
|
meantime — fine for small / fresh clusters, but admins of large
|
|
Postgres releases switching mid-flight should run an ad-hoc
|
|
BackupJob immediately after enabling the flag.
|
|
- The chart-emitted <release>-s3-creds Secret is also skipped, so a
|
|
default install no longer leaks placeholder credentials into the
|
|
tenant namespace.
|
|
*/}}
|
|
{{- $destinationPath := .Values.backup.destinationPath }}
|
|
{{- $endpointURL := .Values.backup.endpointURL }}
|
|
{{/*
|
|
accessKeyIDKey/secretAccessKeyKey only apply when an external Secret was
|
|
supplied via backup.s3CredentialsSecret.name. The chart-managed
|
|
<release>-s3-creds Secret materialised by backup-secret.yaml hard-codes
|
|
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY, so applying overrides in
|
|
that mode would point CNPG at non-existent keys and break credential
|
|
reads. Default both names unconditionally and let the override take
|
|
effect only in external-Secret mode.
|
|
*/}}
|
|
{{- $credAccessKey := "AWS_ACCESS_KEY_ID" }}
|
|
{{- $credSecretKey := "AWS_SECRET_ACCESS_KEY" }}
|
|
{{- if $credCfg.name }}
|
|
{{- $credAccessKey = $credCfg.accessKeyIDKey | default "AWS_ACCESS_KEY_ID" }}
|
|
{{- $credSecretKey = $credCfg.secretAccessKeyKey | default "AWS_SECRET_ACCESS_KEY" }}
|
|
{{- end }}
|
|
{{/*
|
|
endpointCA references a Secret carrying the CA bundle the barman-cloud
|
|
plugin should trust. Required when the S3 endpoint serves a self-signed
|
|
certificate (the cozystack default seaweedfs-s3 ingress qualifies). The
|
|
same reference flows into both the backup ObjectStore's
|
|
spec.configuration.endpointCA and the recovery ObjectStore's
|
|
spec.configuration.endpointCA (referenced from externalClusters[].plugin)
|
|
so backup *and* recovery can verify TLS - missing it on the recovery
|
|
side made bootstrap.recovery panic in CNPG's instance-manager.
|
|
*/}}
|
|
{{- $caCfg := default (dict) .Values.backup.endpointCA }}
|
|
{{- $caKey := $caCfg.key | default "ca.crt" }}
|
|
{{/*
|
|
The barman-cloud plugin is chart-wired only when the legacy path has
|
|
real coordinates:
|
|
- backup.enabled=true AND
|
|
- useSystemBucket=false (platform flow lets the CNPG driver SSA-
|
|
patch spec.plugins + the ObjectStore instead) AND
|
|
- destinationPath is non-empty AND either an inline access/secret
|
|
pair is supplied OR an external Secret reference (s3Credentials-
|
|
Secret.name) is set.
|
|
Anything short of that means the user has enabled backup but not yet
|
|
supplied S3 coordinates — wiring the plugin against a half-empty
|
|
ObjectStore would just feed CNPG a broken archive_command. Tenants
|
|
opting into `useSystemBucket: true` keep spec.plugins unset until the
|
|
backup-controller's CNPG driver SSA-patches it on the first BackupJob.
|
|
|
|
The S3/barman settings live in the barmancloud.cnpg.io/ObjectStore CR
|
|
rendered at the bottom of this file (named after the release); the
|
|
Cluster only references it by name. Native spec.backup.barmanObjectStore
|
|
is deprecated in CNPG 1.27 and removed in 1.29. The bare (system-flavor)
|
|
image tags this chart pins still ship the barman-cloud-* binaries the
|
|
native path needs (unlike the `standard` variant keycloak-db pins), but
|
|
the system flavor is deprecated upstream, so this chart moves to the
|
|
plugin as well.
|
|
*/}}
|
|
{{- $hasInlineCreds := and (ne (default "" .Values.backup.s3AccessKey) "") (ne (default "" .Values.backup.s3SecretKey) "") }}
|
|
{{- $hasLegacyCreds := or $hasInlineCreds (ne (default "" $credCfg.name) "") }}
|
|
{{- $renderBarman := and .Values.backup.enabled (not .Values.backup.useSystemBucket) (ne $destinationPath "") $hasLegacyCreds }}
|
|
{{- if $renderBarman }}
|
|
plugins:
|
|
- name: barman-cloud.cloudnative-pg.io
|
|
isWALArchiver: true
|
|
parameters:
|
|
barmanObjectName: {{ .Release.Name }}
|
|
# Pin the WAL-archive S3 prefix explicitly (the plugin would default it
|
|
# to the Cluster name anyway); the driver preserves it on a live cluster.
|
|
# On restore bootstrap.newServerName overrides it with a fresh prefix, so
|
|
# the re-bootstrapped cluster does not archive onto the recovery source.
|
|
serverName: {{ .Values.bootstrap.newServerName | default .Release.Name }}
|
|
{{- end }}
|
|
|
|
{{/*
|
|
bootstrap.enabled + backup.useSystemBucket is unsupported in this
|
|
chart: bootstrap.recovery requires an explicit externalClusters entry
|
|
with destinationPath / endpointURL / s3Credentials, but useSystemBucket
|
|
deliberately leaves the platform coordinates out of values.yaml (they
|
|
live in the backupstrategy-controller chart). Rendering the bootstrap
|
|
block here with empty coordinates would produce a broken cnpg.io
|
|
Cluster CR and a confusing CNPG bootstrap failure.
|
|
|
|
The supported way to restore against the platform bucket is a
|
|
backups.cozystack.io/RestoreJob targeting a fresh Postgres app — the
|
|
backup-controller's CNPG driver SSA-patches the Cluster's
|
|
spec.bootstrap before CNPG sees it. See the BackupClass and Restore
|
|
flow in docs/operations/backup-classes.md.
|
|
|
|
Fail fast at chart-render time so tenants see the misconfiguration
|
|
immediately rather than during a Postgres bootstrap pod crash loop.
|
|
*/}}
|
|
{{- if and .Values.bootstrap.enabled .Values.backup.useSystemBucket }}
|
|
{{- fail "postgres: bootstrap.enabled=true is incompatible with backup.useSystemBucket=true. Use a backups.cozystack.io/RestoreJob against the cozy-default BackupClass to restore from the system bucket; see docs/operations/backup-classes.md." }}
|
|
{{- end }}
|
|
{{/*
|
|
The externalClusters bootstrap block below references $credSecretName
|
|
(default <release>-s3-creds) for recovery. That Secret is only
|
|
materialised by backup-secret.yaml when inline creds are present, so a
|
|
legacy-flow bootstrap with no usable credentials would render a cnpg.io
|
|
Cluster whose recovery s3Credentials points at a Secret that never
|
|
exists — CNPG then crash-loops with an opaque "secret not found".
|
|
Fail closed at render time (mirroring the useSystemBucket guard above)
|
|
so the misconfiguration is obvious immediately. useSystemBucket-mode
|
|
restore is the RestoreJob flow, already rejected by the guard above.
|
|
*/}}
|
|
{{- if and .Values.bootstrap.enabled (not .Values.backup.useSystemBucket) (not $credCfg.name) (not $hasInlineCreds) }}
|
|
{{- fail "postgres: bootstrap.enabled=true requires S3 credentials: set backup.s3AccessKey+backup.s3SecretKey, backup.s3CredentialsSecret.name, or backup.useSystemBucket=true (with a backups.cozystack.io/RestoreJob)." }}
|
|
{{- end }}
|
|
{{- if .Values.bootstrap.enabled }}
|
|
bootstrap:
|
|
recovery:
|
|
source: {{ .Values.bootstrap.oldName }}
|
|
{{- if or .Values.bootstrap.recoveryTime .Values.bootstrap.backupID }}
|
|
recoveryTarget:
|
|
{{- with .Values.bootstrap.backupID }}
|
|
backupID: {{ . | quote }}
|
|
{{- end }}
|
|
{{- with .Values.bootstrap.recoveryTime }}
|
|
targetTime: {{ . }}
|
|
{{- end }}
|
|
{{- end }}
|
|
externalClusters:
|
|
- name: {{ .Values.bootstrap.oldName }}
|
|
plugin:
|
|
name: barman-cloud.cloudnative-pg.io
|
|
parameters:
|
|
# References the recovery ObjectStore rendered at the bottom of this
|
|
# file, which carries the source cluster's S3 coordinates. serverName
|
|
# (the source's S3 path prefix) rides on the plugin parameter, not the
|
|
# ObjectStore (the plugin forbids serverName there). It must always be
|
|
# set: the native path defaulted an absent serverName to the external
|
|
# cluster's NAME (= oldName), but the plugin defaults it to the NEW
|
|
# cluster's name — which would read the wrong S3 prefix on restore.
|
|
barmanObjectName: {{ .Release.Name }}-recovery
|
|
serverName: {{ .Values.bootstrap.serverName | default .Values.bootstrap.oldName }}
|
|
{{- end }}
|
|
|
|
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 4 }}
|
|
|
|
{{- include "postgres.flavorGuard" $ }}
|
|
imageName: {{ include "postgres.imageName" $ }}
|
|
enableSuperuserAccess: true
|
|
{{- if .Values._cluster.scheduling }}
|
|
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
|
|
{{- if $rawConstraints }}
|
|
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
|
|
labelSelector:
|
|
matchLabels:
|
|
cnpg.io/cluster: {{ .Release.Name }}
|
|
{{- end }}
|
|
{{- end }}
|
|
postgresql:
|
|
{{- if $tlsRequired }}
|
|
{{- /*
|
|
CloudNativePG offers TLS (ssl=on) but its built-in last rule, host all all
|
|
all, also accepts plaintext, so a client choosing sslmode=disable gets an
|
|
unencrypted session. User rules go after the operator's fixed rules
|
|
(local peer, streaming_replica cert) and before that default rule, and the
|
|
first match wins, so these two lines make remote sessions TLS-only.
|
|
https://cloudnative-pg.io/documentation/current/postgresql_conf/#the-pg_hba-section
|
|
*/}}
|
|
{{- /*
|
|
Same password method as CloudNativePG's own default rule: md5 before
|
|
PostgreSQL 14 (where password_encryption defaults to md5, and md5 also
|
|
accepts SCRAM verifiers), scram-sha-256 from 14 on.
|
|
pkg/management/postgres/configuration.go in cloudnative-pg.
|
|
*/}}
|
|
{{- $pgMajor := trimPrefix "v" .Values.version | atoi }}
|
|
{{- $authMethod := ternary "md5" "scram-sha-256" (lt $pgMajor 14) }}
|
|
{{- /*
|
|
With ssl turned off, hostssl never matches and the reject rule would refuse
|
|
every remote session; stop with an actionable error instead.
|
|
*/}}
|
|
{{- $sslParam := "" }}
|
|
{{- if and .Values.postgresql .Values.postgresql.parameters }}
|
|
{{- $sslParam = toString (dig "ssl" "" .Values.postgresql.parameters) | lower }}
|
|
{{- end }}
|
|
{{- if has $sslParam (list "off" "false" "0" "no") }}
|
|
{{- fail "postgresql.parameters.ssl is off while tls.required is in effect (external: true or tls.required: true): enable ssl or set tls.required: false" }}
|
|
{{- end }}
|
|
pg_hba:
|
|
- hostssl all all all {{ $authMethod }}
|
|
- host all all all reject
|
|
{{- end }}
|
|
{{- /*
|
|
Preloading is exposed as a closed allowlist rather than through the blocked
|
|
shared_preload_libraries parameter. The parameter is free text and accepts a
|
|
path, so it can point at a writable location; a bare name from this list can
|
|
only resolve inside $libdir, which lives on the read-only image filesystem.
|
|
The names below all ship with the CloudNativePG PostgreSQL image. The enum in
|
|
values.schema.json is the primary guard; this check backs it up for renders
|
|
that bypass schema validation. helm-unittest cannot reach it, since the schema
|
|
rejects first, so hack/check-postgres-preload-allowlist.bats pins it instead.
|
|
*/ -}}
|
|
{{- with .Values.postgresql.sharedPreloadLibraries }}
|
|
{{- $allowedPreload := list "pg_stat_statements" "auto_explain" "pgaudit" "pg_prewarm" }}
|
|
shared_preload_libraries:
|
|
{{- range . }}
|
|
{{- if not (has . $allowedPreload) }}
|
|
{{- fail (printf "SECURITY: shared preload library %q is not allowed. Allowed: %s. Loading a library from an arbitrary path enables code execution inside the instance." . (join ", " $allowedPreload)) }}
|
|
{{- end }}
|
|
- {{ quote . }}
|
|
{{- end }}
|
|
{{- end }}
|
|
parameters:
|
|
{{- $defaults := dict "max_wal_senders" "30" }}
|
|
{{- /*
|
|
PostgreSQL sizes shared_buffers at 128MB whatever the container gets.
|
|
The pool is shared memory, which the kernel cannot reclaim without
|
|
swap, so every page that fills stays charged against the memory limit.
|
|
A full pool is the whole limit on t1.nano and half of it on t1.micro,
|
|
the default preset. A small or idle database never fills it, but a
|
|
dump, index scans over a large table or a replica catching up after a
|
|
failover can, and then the OOM killer takes the instance. Below 512Mi
|
|
the default is therefore a quarter of the memory limit with a 16MB
|
|
floor, capped at the memory request: CloudNativePG rejects a Cluster
|
|
whose shared_buffers exceeds that request. From 512Mi up a quarter
|
|
is at least 128MB, so nothing is
|
|
rendered and PostgreSQL keeps its own default as before. An explicit
|
|
shared_buffers in postgresql.parameters still wins. The limit is read
|
|
through the same cozy-lib helper that fills spec.resources, so presets
|
|
and resources overrides resolve exactly as they do there.
|
|
*/}}
|
|
{{- $resources := include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | fromYaml }}
|
|
{{- with $resources.limits.memory }}
|
|
{{- /* 4194304 is 4Mi, so bytes divided by it is a quarter of the limit in MiB. PostgreSQL reads MB as MiB. */}}
|
|
{{- $quarterMi := div (include "cozy-lib.resources.toFloat" . | float64 | int64) 4194304 }}
|
|
{{- if lt $quarterMi 128 }}
|
|
{{- $requestMi := div (include "cozy-lib.resources.toFloat" $resources.requests.memory | float64 | int64) 1048576 }}
|
|
{{- $_ := set $defaults "shared_buffers" (printf "%dMB" (min $requestMi (max 16 $quarterMi))) }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- $params := merge (dict) .Values.postgresql.parameters $defaults }}
|
|
{{- /* Security: Block parameters that enable arbitrary code execution (shell commands or .so loading) */ -}}
|
|
{{- $dangerousParams := list "archive_command" "restore_command" "ssl_passphrase_command" "archive_cleanup_command" "recovery_end_command" "dynamic_library_path" "local_preload_libraries" "session_preload_libraries" "shared_preload_libraries" }}
|
|
{{- range $key, $value := $params }}
|
|
{{- $normalizedKey := lower $key }}
|
|
{{- if has $normalizedKey $dangerousParams }}
|
|
{{- fail (printf "SECURITY: Parameter '%s' is blocked. This parameter can enable arbitrary code execution and must not be set by tenants." $key) }}
|
|
{{- end }}
|
|
{{ $key }}: {{ $value | toString | quote }}
|
|
{{- end }}
|
|
|
|
minSyncReplicas: {{ .Values.quorum.minSyncReplicas }}
|
|
maxSyncReplicas: {{ .Values.quorum.maxSyncReplicas }}
|
|
|
|
monitoring:
|
|
enablePodMonitor: true
|
|
|
|
storage:
|
|
size: {{ required ".Values.size is required" .Values.size }}
|
|
{{- with .Values.storageClass }}
|
|
storageClass: {{ . }}
|
|
{{- end }}
|
|
|
|
inheritedMetadata:
|
|
labels:
|
|
policy.cozystack.io/allow-to-apiserver: "true"
|
|
app.kubernetes.io/name: postgres.apps.cozystack.io
|
|
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
---
|
|
apiVersion: cozystack.io/v1alpha1
|
|
kind: WorkloadMonitor
|
|
metadata:
|
|
name: {{ $.Release.Name }}
|
|
labels:
|
|
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
|
|
spec:
|
|
{{- /* Under active autoscaling the desired-replica count is set to the quorum FLOOR
|
|
(effectiveMin), NOT the seeded spec.instances (activeSeed = max(replicas,
|
|
effectiveMin)) and not KEDA's live autoscaled target: the seed is only the
|
|
starting count, KEDA owns the runtime count via /scale and legitimately idles the
|
|
cluster DOWN to effectiveMin under low load. NOTE the health decision does not
|
|
actually read this field today — WorkloadMonitorReconciler gates Status.Operational
|
|
solely on availableReplicas < minReplicas (below), so with minReplicas: 1 an idle
|
|
autoscaled cluster never false-alarms regardless of what replicas says. replicas is
|
|
the declared desired target (see the field doc: "desired number of replicas");
|
|
pinning it to effectiveMin keeps that target honest — the floor KEDA never goes
|
|
below, so should a future reconciler compare desired vs observed it still holds
|
|
actual >= desired, rather than false-flagging every idle-to-floor as the live
|
|
count (or the >floor seed) would. */}}
|
|
replicas: {{ if $autoscaling }}{{ include "postgres.autoscaling.effectiveMin" . }}{{ else }}{{ .Values.replicas }}{{ end }}
|
|
minReplicas: 1
|
|
kind: postgres
|
|
type: postgres
|
|
selector:
|
|
app.kubernetes.io/name: postgres.apps.cozystack.io
|
|
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
version: {{ $.Chart.Version }}
|
|
{{- if $renderBarman }}
|
|
---
|
|
# Backup ObjectStore for the barman-cloud plugin. Carries the S3/barman
|
|
# settings the deprecated spec.backup.barmanObjectStore used to hold; the
|
|
# Cluster references it by name via spec.plugins above. serverName is
|
|
# intentionally omitted (the plugin forbids it here and defaults it to the
|
|
# cluster name, preserving the s3://.../<cluster>/ layout).
|
|
apiVersion: barmancloud.cnpg.io/v1
|
|
kind: ObjectStore
|
|
metadata:
|
|
name: {{ .Release.Name }}
|
|
spec:
|
|
{{- if .Values.backup.retentionPolicy }}
|
|
retentionPolicy: {{ .Values.backup.retentionPolicy | quote }}
|
|
{{- end }}
|
|
# Size the barman-cloud sidecar explicitly instead of inheriting the tenant
|
|
# LimitRange default of 128Mi, and pin its S3 request checksum (rationale in
|
|
# cozy-lib.barman.sidecarConfiguration).
|
|
{{- include "cozy-lib.barman.sidecarConfiguration" . | nindent 2 }}
|
|
configuration:
|
|
destinationPath: {{ $destinationPath }}
|
|
endpointURL: {{ $endpointURL }}
|
|
{{- if $caCfg.name }}
|
|
endpointCA:
|
|
name: {{ $caCfg.name }}
|
|
key: {{ $caKey }}
|
|
{{- end }}
|
|
s3Credentials:
|
|
accessKeyId:
|
|
name: {{ $credSecretName }}
|
|
key: {{ $credAccessKey }}
|
|
secretAccessKey:
|
|
name: {{ $credSecretName }}
|
|
key: {{ $credSecretKey }}
|
|
{{- end }}
|
|
{{- if .Values.bootstrap.enabled }}
|
|
---
|
|
# Recovery ObjectStore for bootstrap. Points at the source cluster's S3
|
|
# coordinates; referenced from externalClusters[].plugin above. serverName
|
|
# rides on that plugin parameter, not here.
|
|
apiVersion: barmancloud.cnpg.io/v1
|
|
kind: ObjectStore
|
|
metadata:
|
|
name: {{ .Release.Name }}-recovery
|
|
spec:
|
|
# Same sidecar configuration as the backup ObjectStore above.
|
|
{{- include "cozy-lib.barman.sidecarConfiguration" . | nindent 2 }}
|
|
configuration:
|
|
destinationPath: {{ $destinationPath }}
|
|
endpointURL: {{ $endpointURL }}
|
|
{{- if $caCfg.name }}
|
|
endpointCA:
|
|
name: {{ $caCfg.name }}
|
|
key: {{ $caKey }}
|
|
{{- end }}
|
|
s3Credentials:
|
|
accessKeyId:
|
|
name: {{ $credSecretName }}
|
|
key: {{ $credAccessKey }}
|
|
secretAccessKey:
|
|
name: {{ $credSecretName }}
|
|
key: {{ $credSecretKey }}
|
|
{{- end }}
|