The pin check matched job="kube-state-metrics" anywhere in the trigger query, so
one pinned selector satisfied it no matter what the other three did: unpinning
$joinReplica, $idleFloor or $joinCluster individually left the suite green while
the join went back to matching any exporter's series. Anchor one assertion per
selector on the label that identifies its site so a single dropped pin fails.
Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The KEDA-missing fail-closed guard's comment still described the replaced
drop-instances design ("db.yaml drops spec.instances ... CNPG collapses to 1");
under the shipped seed design db.yaml seeds instances at max(replicas,
effectiveMin), so a skipped ScaledObject leaves a fixed-size cluster with no HPA
- autoscaling silently absent, not a collapse. Correct the rationale. Also note
on the cozyrds apply-strategy annotation that the client-side switch rolls out
per-app as each Application is next written through the aggregated API, not
fleet-wide at upgrade (the ApplicationDefinition reconciler syncs only
chartRef/valuesFrom/labels onto a live HelmRelease). Follow-up from #3954 (#4187).
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The read-load trigger query joined on kube_pod_labels without pinning
job="kube-state-metrics", while the sibling DatabaseAutoscaler* alert rules
already pin it. A second exporter emitting a kube_pod_labels series (or a
duplicate scrape) would multiply the join and distort the desired-count
arithmetic. Pin the job on every kube_pod_labels selector in the query for
parity with the alerts. Follow-up from #3954 (#4187).
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
## What this PR does
Adds automatic horizontal autoscaling of a PostgreSQL database's **read
replicas** in response to load, implementing the merged design proposal
[Database Horizontal
Autoscaler](https://github.com/cozystack/community/tree/main/design-proposals/database-horizontal-autoscaling)
(community#44). The mechanism is entirely stock — no bespoke operator,
no new CRD:
- **KEDA as an optional platform component** (`packages/system/keda`,
wired into `core/platform` as a source + system-bundle entry). It claims
the `external.metrics.k8s.io` APIService that nothing served before.
- **`cozy-lib.keda.scaledObject` helper**
(`packages/library/cozy-lib/templates/_keda.tpl`) renders a KEDA
`ScaledObject`, fail-closed on missing/invalid arguments.
- **`packages/apps/postgres` autoscaling** — an `autoscaling` values
block (`enabled`, `minReplicas`, `maxReplicas`, `metric`, `target`,
`maxReplicationLagSeconds`, `dryRun`, `transition`) validated by
`values.schema.json`; under active autoscaling the chart renders a
`ScaledObject` and seeds `spec.instances` to a constant floor
(`max(replicas, effectiveMin)`), while the postgres release is forced to
client-side (two-way) Helm apply so the seed is a merge no-op on upgrade
and the KEDA-managed HPA — the runtime writer via the CNPG `Cluster`
scale subresource — is not reverted.
- **Single-value metric (§1)** — one PromQL query returns `Σ(read load
over standbys) + target`, so a stock HPA computes `desired =
ceil((Σ+target)/target) = 1 + ceil(Σ/target) = primary + desiredRead`,
with no per-pod emission.
- **The four static brakes (§5)** — quorum floor `max(minReplicas,
maxSyncReplicas+1, 2)` (quorum wins over `maxReplicas`), scale-down
pacing (`Pods/1/600s`), a replication-lag clamp folded into the query
(freeze both directions while lag exceeds the threshold and the primary
is writing, with `max_over_time` hysteresis), and dry-run (render
paused).
- **Two-phase enable/disable migration** via a `transition` sub-flag:
phase 1 stages `spec.instances` to the live count and brings KEDA up
with its HPA paused in both directions; flipping to phase 2 hands the
runtime count to KEDA, and because the seed re-renders the same count
the flip is a merge no-op with no dip.
- **Observability** — a database-autoscaling Grafana dashboard and
PrometheusRule alerts.
**Live validation (dev cluster, CNPG 1.30.0).** The full path works
end-to-end: the CNPG `Cluster` scale subresource serves
`status.selector`, KEDA renders the HPA, the metric reaches it
(`ScalingActive=ValidMetricFound`, `TARGETS 151/150`, `desiredReplicas=2
= 1 primary + 1 replica` — the §1 arithmetic), and the lag-clamp query
returns the base value on live VictoriaMetrics. Two packaging fixes were
needed and are included: KEDA must run on the cozystack `cozy.local`
cluster domain (the upstream `cluster.local` default makes the metrics
gRPC target NXDOMAIN and every HPA hangs on `FailedGetExternalMetric`),
and the operator's client-go throttle is raised so a single-replica
operator does not lose its leader lease under scale-subresource polling.
Implements #3953.
### Downstream repositories
Walked the trigger map in `docs/agents/contributing.md` against the
diff. Two repositories are affected; both are follow-ups to open once
this lands (and after its dependency #3951 merges and the values surface
is final), left unticked here for a maintainer to confirm rather than
claimed with a speculative link:
- **cozystack/terraform-provider-cozystack** — this adds the nested
`autoscaling` block to `packages/apps/postgres/values.schema.json`. The
provider hand-models each app's fields (schema + model +
expand/flatten), so it needs the new block to expose it in Terraform.
- **cozystack/website** — KEDA is a new platform component
(`guides/platform-stack`), and the postgres reference page gains the
`autoscaling` params. The managed-app reference is regenerated from
`README.md` by the release bot; the platform-stack component list is
hand-maintained.
- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up: to open (new platform component + postgres autoscaling
params)
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up: to open (new `autoscaling` block in postgres
values.schema.json)
### Depends on
- #3951 — bump CloudNativePG to 1.30.0. The HPA can only drive the CNPG
`Cluster` scale subresource on CNPG ≥ 1.28.4, where it serves
`status.selector`; without it the HPA fails with `InvalidSelector`. This
PR should not merge before #3951.
### Release note
```release-note
feat(postgres): add optional horizontal autoscaling of read replicas via KEDA, configured through the new `autoscaling` values block; KEDA ships as a new optional platform component. Requires CloudNativePG >= 1.28.4 for the Cluster scale subresource selector. Note: every Postgres release now applies client-side (`release.cozystack.io/helm-server-side-apply: "false"`) so the autoscaler's live `/scale` count is not reverted by Helm; a kind-wide side effect is that chart-rendered fields on Postgres releases are no longer re-asserted by server-side-apply drift correction (driftDetection is unset), a documented trade-off with a per-instance apply strategy as follow-up.
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added configurable PostgreSQL read-replica autoscaling with KEDA.
* Added CPU and read-connection metrics, replica bounds, replication-lag
safeguards, dry-run mode, and transition handling.
* Added optional KEDA platform support, Grafana autoscaling dashboard,
and Prometheus alerts.
* **Documentation**
* Clarified integer thresholds, prerequisites, configuration, and
transition guidance.
* **Bug Fixes**
* Improved validation and actionable errors for unavailable KEDA or
invalid settings.
* **Tests**
* Expanded coverage for autoscaling, observability, package selection,
and validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The serverAddress host regex accepted any dotted tail ending in .svc, so a
single-label host like foo.svc passed the in-cluster-DNS check. It then reached
the namespace-pin, whose extraction regex left the unresolvable host unchanged
and rejected it as a namespace outside the tenant subtree - the right outcome by
the wrong path, with a misleading message. Anchor the host to two DNS labels
before .svc so it is refused at the host check with the actionable message, and
drop the stray dot the old label class allowed.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Closes IvanHunters' MINOR on scaledobject.yaml:97. The API-owner-gate marker was a Go
template comment, stripped at render, so the ownership question and the pin's residual
surface were recorded where nobody reads them again. State the constraint on the
serverAddress field doc (propagated into values.schema.json, README and the cozyrds
openAPISchema) including the accepted residual: tenant-root is always in the allow list
because the derived default points there, so a tenant can still aim the operator at a root
Service as a blind in-cluster probe (no response body reaches the tenant), which the egress
NetworkPolicy does not narrow. The API-owner sign-off is tracked on the PR, not in a
stripped comment.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Closes IvanHunters' MINOR on values.yaml:58. The replicas description (propagated into
values.schema.json, README and the cozyrds openAPISchema) said the count is "restored on
disable". Under client-side apply it is not: the two-way merge writes instances only when
the rendered value changes, so a cluster KEDA grew past replicas stays there after
autoscaling.enabled: false with no autoscaler left to reduce it — the sibling dryRun doc
already states the true rule, so the two contradicted each other. Reword both the @param
and the @typedef to say the live count is not auto-restored and that the operator lowers
replicas to reclaim the grown capacity; regenerated the derived artifacts.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Closes IvanHunters' MAJOR on scaledobject.yaml:39. The render guard rejected
replicas > maxReplicas but not the symmetric low side. Under dry-run/transition the
chart renders the static instances=replicas while the ScaledObject carries
minReplicaCount=effectiveMin with both scale directions paused. The pause disables
the HPA's scaling policies, not its min-replica clamp, so when replicas < effectiveMin
the HPA raises the live count to the floor on its first reconcile — a scale-up in a
mode documented as "actuates neither way" — and because the rendered instances value
never changes, the client-side two-way patch never pulls it back, leaving permanent
extra standbys and PVCs. Fail closed and tell the operator to stage replicas to the
floor first. Under dry-run/transition this floor guard is strictly broader than the
db.yaml quorum wedge (any replicas <= maxSyncReplicas is also < effectiveMin), so it
supersedes it there; the wedge stays exercised on the disabled path.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The lag-brake frozen summand's `or vector(0)` comment claimed it prevents a
silent pin-to-floor when the pod-label count is momentarily empty under load.
That state is unreachable: the frozen count selects the same kube_pod_labels
series the base load term joins on, so base and frozen are empty or non-empty
together and the floor never changes the result. State that it is a no-op marker.
The enablement note framed raising the quorum floor / minReplicas as purely
upward, but the merge rebases to the newly-rendered seed vs the previous seed,
never to KEDA's live count — so raising the floor (or lowering replicas) to a seed
still below a load-scaled live count sheds the surplus replicas and PVCs in one
step before the HPA re-clones. Add the HAZARD note.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The serverAddress SSRF guard had two holes beyond the external-domain suffix
already closed. It extracted the host by truncating at the first `:` or `/`,
which a `http://<svc>.<ns>.svc:port@evil.example.com/` userinfo payload defeats:
the truncated text passes the `.svc` check while net/url dials the host after the
`@`. And it validated only the host shape, never the namespace, so a tenant could
point the cluster-privileged KEDA operator at a sibling tenant's or a system
namespace's Service (the egress NetworkPolicy's `cluster` entity does not stop
in-cluster egress). Parse the URL authority properly (scheme, path/query/fragment,
userinfo to the last `@`, then port) and pin the host's namespace to the tenant's
own subtree via cozy-lib.rbac.allParentTenantsAndThis. The namespace pin narrows
what a tenant may set and needs API-owner sign-off (noted at the guard).
Separately, nothing bounded maxReplicas: the schema carries no maximum and the
aggregated apiserver does not range-validate the CR, so a tenant could set
maxReplicas: 100000 and drive KEDA to scale CNPG toward that count. Add a render
guard rejecting a maxReplicas above a sane read-replica ceiling.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The comment claimed setting replicas to effectiveMin prevents a false
under-provisioning alarm when KEDA idles the cluster to its floor. But
WorkloadMonitorReconciler gates Status.Operational solely on
availableReplicas < minReplicas and never reads spec.replicas, so with
minReplicas: 1 the field is inert for health today. Keep the value (it is the
honest desired-floor target per the field doc) but state what actually drives
the alert, so the rationale is not read as describing behaviour the controller
does not have.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The serverAddress in-cluster check accepted an unanchored trailing domain
(`\.svc(\.[a-zA-Z0-9.-]+)?$`), so a crafted host like x.svc.attacker.com — a
`.svc` label followed by any external domain — passed the guard. A tenant could
then point the cluster-privileged KEDA operator off-cluster (the egress
NetworkPolicy is the only remaining barrier, and it is inert where the CNI does
not enforce CiliumNetworkPolicy).
Anchor the optional suffix to this cluster's domain (from _cluster.cluster-domain,
injected by the platform from networking.clusterDomain): a legitimate vmselect is
<service>.<namespace>.svc or ...svc.<clusterDomain>, nothing else. The prior tests
only exercised non-.svc hosts and gave false confidence; add a case for the
.svc.<external> bypass.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Close two render-guard follow-ups the design proposal names for this PR:
- Fail closed when replicas exceeds autoscaling.maxReplicas. The stock HPA's
currentReplicas > maxReplicaCount branch writes /scale = maxReplicaCount
unconditionally on the first reconcile — even under the both-direction pause of
dryRun/transition — so a resting/seed count above the ceiling would shed the
surplus standbys and their PVCs in one step, past the scale-down pacing. Mirrors
the existing effectiveMin > maxReplicas guard.
- Refuse an autoscaling.serverAddress whose host is not an in-cluster Service DNS
name (…svc[.<domain>]). The cluster-privileged KEDA operator fetches it every
poll, so an external/IP address is an SSRF surface (cloud metadata endpoint,
another tenant); the query body is already namespace-scoped, this restricts the
destination. Defense-in-depth pairs with the operator egress policy (keda package).
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Under active read-replica autoscaling the chart seeds spec.instances at a
constant max(replicas, effectiveMin) rather than omitting it (an omitted field
defaults to 1, which the CNPG quorum webhook rejects, and — under server-side
apply — the phase-2 omit prunes the field whenever KEDA has not yet written
/scale, collapsing an idle-at-floor cluster to 1 and shedding its replica PVCs).
Keeping the constant only works if Flux does not force-own and revert it. The
postgres ApplicationDefinition therefore carries
release.cozystack.io/helm-server-side-apply: "false", so the tenant HelmRelease
applies client-side: helm-controller patches the CNPG Cluster from the
previous-vs-new rendered manifest without consulting live state, the unchanged
constant yields no patch, and KEDA's higher live /scale count is preserved. The
constant also equals the disabled/dryRun/transition render (instances: replicas)
whenever replicas sits at/above the floor, so those flips are patch-free too.
Correct the merge-semantics comments across db.yaml, _autoscaling.tpl and
values.yaml to describe the two-way previous-vs-new patch precisely (not a
"three-way no-op"), and document the residual floor-change hazard: editing
replicas / minReplicas / quorum.maxSyncReplicas moves the seed, so that one
reconcile does patch spec.instances to the new seed. Add autoscaling.serverAddress
so a monitoring:true tenant can point the ScaledObject at a vmselect that carries
both the CNPG metrics and kube_pod_labels. Regenerated README/schema/types/cozyrds.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Address review findings on the KEDA read-replica autoscaler where the
implementation diverged from the merged design proposal.
Fail-safe read load. Gate the `or vector(0)` floor on the existence of ANY CNPG
instance pod (`count(kube_pod_labels{...instance_role=~".+"}) > 0`) — primary or
replica — so the query is empty ONLY when no CNPG pod labels exist at all (a
broken kube-state-metrics pipeline), not during initial provisioning when the
sole replica-side pod is CNPG's join Job (jobRole, not instanceRole). Also pin
`ignoreNullValues: "false"` on the trigger: KEDA's prometheus scaler defaults it
true, turning an EMPTY result into metric 0 (not an error) — the HPA would then
desire 0 and clamp to the floor, i.e. silently scale a broken-pipeline cluster
DOWN. With it false an empty result is a scaler error, so the HPA HOLDS and
DatabaseAutoscalerScalerErrors fires; gating on any instance role keeps a healthy
cluster (including a long basebackup) from tripping that alert.
Constant seed = max(replicas, effectiveMin), so enable/dryRun/transition flips
are a merge no-op with no dip and never undercut the quorum floor.
Fail closed when the quorum floor exceeds maxReplicas (was: effectiveMax silently
raised both bounds, unbounded for a large maxSyncReplicas).
Dry-run / transition keep the HPA (unidirectional pause pair, not the HPA-deleting
full pause) but both SelectPolicy=Disabled, so desired normalizes to current — the
recommendation is read from the read-load metric, not a diverging desired count.
Observability. DatabaseAutoscalerAtMax keys on desired >= maxReplicas, NOT the
ScalingLimited condition (which is also true at the floor, so it fired on every
idle cluster with wrong "raise maxReplicas" advice). Dashboard read-load panels
are per-cluster (label_cnpg_io_cluster) with a CPU panel for ReadCPUUtilization
and the exact clamp in descriptions; ScaleStuck's Pending intersection is scoped
to CNPG pods; alerts.yaml narrative comments moved to Go-template comments so they
do not ship into the PrometheusRule.
Guard the seeding invariant: TestBuildHelmReleaseSpecNoForceOrDriftDetection.
Regenerated README/schema/types.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The disable case (3) note claimed disabling snaps instances back to
.Values.replicas, but the client-side three-way merge compares the newly
rendered value against the previously applied one, not the live count.
When replicas == effectiveMin (the default) it is a no-op and KEDA's live
count is preserved (silent over-provisioning), so it does not snap back.
Reword case (3) and the "differs from the live one" framing to match the
actual merge semantics.
Re-point the no-Force/no-SSA invariant citation from
internal/controller/applicationdefinition_helmreconciler.go (which holds
no apply-strategy code) to internal/operator/package_reconciler.go
buildHelmReleaseSpec, where the HelmRelease spec is actually built.
Comment/doc only; helm unittest 52/52 unchanged.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Address IvanHunters' deep review round on #3954:
- [MAJOR] Enabling autoscaling directly on an existing non-quorum database no
longer collapses it to 1 instance and deletes every replica PVC. The chart now
SEEDS spec.instances at the quorum floor (effectiveMin) on BOTH the quorum and
non-quorum active paths instead of omitting the field in the non-quorum case;
the constant seed is a three-way-merge no-op so KEDA still owns the runtime
count via /scale, and the enable dip is capped at the floor (>=2) rather than
collapsing to 1. Documents the client-side-3-way-merge (not SSA) assumption
that the no-op relies on.
- [MINOR] Fail closed on autoscaling.minReplicas > autoscaling.maxReplicas instead
of silently resolving the ceiling upward (which defeated maxReplicas as a cost
knob). The stray maxReplicas: 0 case now rejects rather than clamps.
- [MINOR] The disable-wedge guard, its values note and its test no longer emit the
invalid `kubectl scale --subresource=scale` (scale has no --subresource); use
`kubectl scale cluster.postgresql.cnpg.io/<name> --replicas=N`.
- [MINOR] Correct the enablement note: raising minReplicas/maxSyncReplicas on a
live cluster is NOT harmless when KEDA's live count exceeds the new floor (it
decommissions surplus replicas + PVCs); prescribe pin-first for raises too.
- [MINOR] Add DatabaseAutoscalerKedaOperatorDown (absent(keda_build_info)): when
the operator is unhealthy a ScaledObject never becomes an HPA and no other alert
can fire. Intersect DatabaseAutoscalerScaleStuck with a Pending pod so a healthy
long basebackup (Running pod, desired>current >15m) no longer false-fires.
- [MINOR] Keep the KEDA CRDs on package uninstall (crds.additionalAnnotations
resource-policy: keep via the shim) so removing the keda package doesn't
cascade-delete every tenant ScaledObject.
- [MINOR] cozy-lib helper: reject a non-numeric bound (int "two" -> 0 -> a
scale-to-zero ScaledObject) and render an explicit pollingInterval/cooldownPeriod
of 0 (`if not kindIs invalid`, not `with`).
- [MINOR] Note the read-load fail-open: a broken metrics join reads as idle and
silently pins autoscaling at the floor under real load.
New/updated tests cover the non-quorum seed, the min>max guard, the corrected
kubectl message, the operator-down + Pending-intersected alerts, the CRD keep
policy, and the non-numeric-bound helper guard.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The autoscaling instance-count explanations were plain `#` YAML comments, so they
shipped verbatim into every rendered CNPG Cluster manifest on every customer
cluster (unlike the {{/* */}} comments elsewhere in the chart). Convert them to
Go-template comments so the rationale stays in-source but is stripped at render,
matching the treatment already applied to the apps.yaml clusterDomain comment.
Also document (as a stripped comment) that WorkloadMonitor.spec.replicas tracks
the quorum floor, not KEDA's live autoscaled target, under active autoscaling.
Addresses the deep cozy-review notes on #3954.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The database-autoscaling dashboard hardcoded "datasource": "VictoriaMetrics" on
its namespace template variable, but cozystack provisions VictoriaMetrics
datasources as vm-shortterm / vm-longterm — there is no datasource named
"VictoriaMetrics", so on import the variable query failed ("datasource not
found"), the namespace variable never populated, and every panel (which filters
on namespace=~"$namespace") showed no data. Switch to the repo-standard pattern
used by db/cloudnativepg and the other dashboards: a `datasource`-typed template
variable (ds_prometheus) referenced as { type: prometheus, uid: ${ds_prometheus} }
from the namespace variable and every panel/target (the panels previously carried
no datasource at all and only worked by falling back to the default).
Also reframe the disable-wedge guard message to lead with the invariant (replicas
must exceed quorum.maxSyncReplicas) so a pure-quorum user who never enabled
autoscaling isn't told to "disable autoscaling"; the disable path is kept as a
parenthetical cause.
Addresses the adversarial branch-review findings on #3954.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Address the remaining IvanHunters review points on #3954:
- [MAJOR] Disabling autoscaling wedged the HelmRelease under synchronous quorum.
An autoscaled + maxSyncReplicas>=1 cluster runs with instances seeded from
effectiveMin while replicas stays at its default, so flipping autoscaling off
rendered instances: <replicas> next to the quorum and CNPG's webhook rejected
it ("maxSyncReplicas must be lower than the number of instances"). Add a
symmetric render-time guard on the static-count path (covers off/dryRun/
transition): fail with an actionable message when replicas <= maxSyncReplicas.
- [MINOR] Role-scope the freeze brake's currentInstances count
(label_cnpg_io_instance_role=~".+") so lingering CNPG join/bootstrap Job pods
(cnpg.io/cluster but jobRole, not instanceRole) don't inflate it and nudge
desired up while the brake is meant to hold it.
- [MINOR] Default maxReplicationLagSeconds to 0 (brake opt-in): only the
non-braking path is validated live, so ship the validated default and let the
freeze be enabled explicitly until its branch is exercised under load.
- [MINOR] Document the floor-change and non-quorum-disable dips (same three-way-
merge hazard) and the pin-replicas-first mitigation.
- [NIT] Coerce the cozy-lib helper's `paused` (eq "true") so a caller passing the
string "false" does not pause the ScaledObject.
- [NIT] Trim the keda.clusterDomain injection comment so it no longer ships ~8
verbatim lines into the cozystack-values Secret on every cluster.
Regenerate types/schema/README/postgres-rd. New tests cover the disable-wedge
guard, the role-scoped freeze count, and the string-"false" paused coercion.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Address the branch-review nits:
- Fail closed when the monitoring namespace is unset. serverAddress builds
vmselect-shortterm.<ns-monitoring>.svc, and an empty ns-monitoring rendered the
malformed vmselect-shortterm..svc:8481, surfacing late as a KEDA scaler error.
The platform always injects it, but a hand-rendered/misconfigured tenant now
gets an actionable render-time failure mirroring the KEDA-CRD and target guards.
Tested.
- Document that the database-autoscaling PrometheusRule matches every KEDA HPA /
ScaledObject error (the underlying series carry no owning-app label), which is
correct only while database autoscaling is KEDA's sole consumer in cozystack;
note to scope the rules if a non-database ScaledObject is ever added.
- Correct the cozy-lib.keda.scaledObject usage example: `paused` should reflect
dry-run OR migration transition, not dry-run alone, so a second engine copying
the example does not drop the transition pause.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Two review notes from IvanHunters:
- effectiveMin/effectiveMax wrapped minReplicas/maxReplicas/maxSyncReplicas/
maxReplicationLagSeconds in Sprig `default`, which treats a numeric 0 as empty
and silently substitutes the fallback (e.g. maxReplicas: 0 became 6, discarding
the tenant's value). values.yaml/schema already default these when omitted, so
read them directly; an explicit out-of-range value now flows into max() and is
clamped to the quorum floor rather than swapped for a different number. Add a
test that maxReplicas: 0 clamps to effectiveMin instead of becoming 6.
- The replication-lag brake only floored the product $braking with `or vector(0)`,
so if a CNPG build stops emitting cnpg_pg_replication_lag or
cnpg_collector_wal_records the individual gate resolved to an empty vector.
Floor each gate ($lagHigh, $writing) individually so the fail-open decision is
explicit and per-gate: the brake engages only on positively-observed lag AND
writing, and a missing signal degrades to "scale normally" (safer than freezing
on absent data) rather than relying on the emptiness of the product. Assert both
gates carry the floor.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The read-load query's base term is floored with `or vector(0)`, but the
replication-lag brake's frozen additive term (count(kube_pod_labels) * target)
was not. PromQL `+` is a set intersection, so if count(kube_pod_labels) is
momentarily empty — a kube-state-metrics scrape gap or KSM rollout — the frozen
term is empty and the whole `base + frozen*braking` sum collapses to empty,
defeating the base term's own graceful degradation. KEDA's prometheus scaler
(ignoreNullValues: true by default) then reads that as 0 and pins desired to
minReplicaCount, ignoring real read load exactly when metrics are flaky. This
hits the default config (maxReplicationLagSeconds: 30, brake active). Floor the
frozen summand with `or vector(0)` so base + 0 = base, and assert the floor in
the lag-brake unittest.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Cover the load-bearing replica selector that both read-load queries depend on:
the join must restrict to standby pods via label_cnpg_io_instance_role="replica",
the exact key kube-state-metrics exposes for CNPG's cnpg.io/instance_role. A
wrong key (e.g. a camelCase label_cnpg_io_instanceRole) would match no series,
floor the read term to 0, and silently pin desired to the minimum - the suite
previously never asserted the selector at all. Verified live on dev9 (CNPG
1.30.0) that the snake_case key is the one present in kube_pod_labels and the
camelCase form does not exist. Adds the assertion for both the ReadConnections
and ReadCPUUtilization paths.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Two review findings:
- The enablement note claimed the two-phase `transition` flow avoids the
count dip when enabling autoscaling on an existing cluster. It does not:
helm-controller upgrades with Helm's three-way merge (not SSA), which keys off
the old-vs-new manifest diff, not KEDA's live /scale value. At phase 2 the
chart stops rendering the staged static count, so the field is deleted (no
quorum -> CNPG defaults to 1, a brief collapse) or changed (quorum -> rebased
to effectiveMin), then the warm KEDA restores it. Rewrite the docs to state the
flow reduces (KEDA is pre-warmed, no cold-start) but does not eliminate the dip,
and spell out the suspend + scale-subresource pin that actually makes the
handoff dip-free. This is the SSA-vs-three-way-merge handoff the design proposal
left for the PoC to settle.
- Pin metricType: AverageValue on the Prometheus trigger in
cozy-lib.keda.scaledObject. The desired = ceil(query / threshold) identity
holds only for an AverageValue external metric; it was relying on KEDA's
default. Make it explicit and assert it so a re-vendor or default change cannot
silently break the arithmetic.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Narrow the previous unconditional seed to a conditional one, keeping design
proposal §3 (chart omits the field, HPA is the sole writer) intact for the
default path. Without synchronous quorum (maxSyncReplicas=0) CNPG's defaulted
instances=1 is admissible, so the field is omitted exactly as §3 specifies and
KEDA owns it via /scale with nothing contested. Only when maxSyncReplicas>=1 -
where an omitted/defaulted instances=1 fails the admission webhook
("maxSyncReplicas must be lower than the number of instances") - does the chart
seed the quorum floor (effectiveMin) so a fresh Cluster is admitted; the constant
re-render keeps three-way merge from reverting KEDA's higher live value.
This confines the deviation to the case §3 did not consider (sync quorum), so no
design-proposal amendment is needed for the documented path. Verified live on a
dev cluster: the quorum render (instances=2, maxSyncReplicas=1) and the
non-quorum render (instances omitted) are both admitted by the CNPG webhook. Adds
a render test for the non-quorum omit path alongside the quorum seed tests.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Under active autoscaling the chart previously omitted .spec.instances entirely,
letting the KEDA HPA own it via /scale. But CNPG defaults an absent
.spec.instances to 1, and its admission webhook rejects a Cluster where
maxSyncReplicas >= instances ("maxSyncReplicas must be lower than the number of
instances"). So a fresh database created with autoscaling.enabled: true AND
synchronous quorum (maxSyncReplicas >= 1) was rejected at admission before KEDA
could ever scale it to effectiveMin (= maxSyncReplicas+1) — the whole tenant
HelmRelease failed. The default maxSyncReplicas: 0 path was unaffected, which is
why it went unnoticed. Verified live: a rendered Cluster with maxSyncReplicas: 1
and an omitted instances is rejected by the dev-cluster webhook; with the seeded
floor it is admitted.
Render instances: effectiveMin under active autoscaling instead of omitting it,
so a fresh Cluster starts at a valid, quorum-satisfying count. Because the chart
emits the same constant floor on every reconcile, Helm's three-way merge is a
no-op on the field and never reverts KEDA's higher live value, so the HPA still
owns the runtime count. Update the enablement docs (a new DB can enable directly
even with synchronous replication; existing DBs above the floor rebase to it
unless the two-phase transition flow is used) and add render tests for the fresh
create + maxSyncReplicas>=1 case.
Addresses the fresh-create-with-quorum admission gap found in branch review.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
autoscaling.enabled is a tenant-facing knob, but KEDA is an optional platform
package that is not installed by default. A tenant enabling autoscaling on a
cluster without KEDA would render a ScaledObject against an unknown kind and the
whole database HelmRelease would fail with an opaque `no matches for kind
"ScaledObject"`. Guard the ScaledObject template on
`.Capabilities.APIVersions.Has "keda.sh/v1alpha1"` and stop with an actionable
message instead. Skipping the render is deliberately NOT an option: db.yaml
already drops the static .spec.instances under active autoscaling, so a missing
ScaledObject would let CNPG collapse the cluster to 1 instance. Document the
KEDA prerequisite on the `enabled` field (README/schema).
Also reject a non-positive autoscaling.target at render time: it is a plain
integer now, but the desired-replica formula divides the read load by it, so 0
is undefined and negatives invert scaling. cozyvalues-gen has no minimum
directive, so the bound is a template guard.
Add unittests for both guards (a dedicated suite renders without the KEDA CRD so
the fail-closed path is exercised; the main suite now advertises keda.sh/v1alpha1
via capabilities).
Addresses IvanHunters (KEDA-absent) and CodeRabbit (non-positive target) review
feedback on #3954.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Two review findings on the generated API:
- autoscaling.target was a resource.Quantity, which accepts suffixed values
like `150m`. The chart splices target straight into the PromQL query and the
KEDA threshold, where `150m` means 150 minutes to PromQL and is not a usable
KEDA float threshold. Retype it as a dimensionless integer so the schema
rejects suffixed input - helm unittest's schema validation now enforces it,
and the existing autoscaling suite exercises the integer contract.
- ConfigSpec carried a root `metric` field (and spec.metric in the postgres-rd
schema) that nothing reads - the chart consumes only autoscaling.metric - so
setting it silently did nothing. Remove the dead root param, keeping the
ReadMetric enum that autoscaling.metric references.
Regenerate types.go, zz_generated.deepcopy.go, values.schema.json, README.md
and the postgres-rd schema; update the autoscaling tests to the integer target.
Addresses CodeRabbit review feedback on #3954.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Wire autoscaling.maxReplicationLagSeconds into the read-load query: while the
max standby lag has exceeded the threshold within the cooldown window and the
primary is actively writing WAL, the query returns currentInstances*target,
pinning desired to the current count and freezing scaling in both directions
(safer than pinning maxReplicas, which would still allow scale-down under
lag). The lag term uses max_over_time as a hysteresis band; the write gate
(WAL-record rate > 0) keeps an idle primary from tripping it. Metrics
verified live on CNPG 1.30.0: cnpg_pg_replication_lag and
cnpg_collector_wal_records exist and carry namespace/pod (the proposal's
cnpg_pg_stat_replication_sent_diff_bytes does not). Also floor the read load
at zero via `or vector(0)` so an idle cluster returns target rather than an
empty result.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Make enabling autoscaling on an existing database count-preserving, and
add a second read-load signal:
- autoscaling.transition (migration phase 1): keep .spec.instances
rendered as a floor (from replicas, which the operator stages to the
live count) AND pause the ScaledObject, so KEDA stands up without
contending with Flux for the field. Flip transition off (phase 2) to
drop the static field and hand the count to KEDA — suspend the
HelmRelease across that apply, since helm-controller uses Helm's
three-way merge (not SSA) and would otherwise race CNPG's default-to-1.
The procedure is documented on the values field.
- autoscaling.metric (ReadConnections | ReadCPUUtilization): the query
helper emits either the active-connections sum or the postgres-container
CPU sum, both + target.
active = enabled && !dryRun && !transition; the ScaledObject is paused
whenever dryRun or transition is set. Covered by helm unittest (phase-1
holds the staged count + pauses; CPU metric query) and regenerated
schema/README/Go types/postgres-rd.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
Implement the KEDA-based read-replica autoscaler for postgres per the
merged design proposal (cozystack/community#44):
- cozy-lib.keda.scaledObject: engine-agnostic helper rendering a KEDA
ScaledObject (scale target, effective bounds, prometheus trigger,
scale-down behavior, optional pause), fail-closed on bad input.
- postgres: an `autoscaling` values block (enabled / minReplicas /
maxReplicas / target / maxReplicationLagSeconds / dryRun). Under active
autoscaling the chart drops the static .spec.instances so Flux and the
KEDA-managed HPA no longer contend, and renders a ScaledObject whose
minReplicaCount folds in the synchronous-quorum floor
(max(minReplicas, maxSyncReplicas+1, 2)); when the floor exceeds
maxReplicas, quorum wins. dry-run keeps the static count and pauses the
ScaledObject. The WorkloadMonitor tracks the quorum floor instead of a
stale static value.
Covered by helm unittest (cozy-lib helper: valid render, pause, three
fail-closed guards; postgres: instances dropped, quorum-floored bounds,
quorum-over-max, dry-run pause). Regenerated values.schema.json, README,
Go types and postgres-rd.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
The comment sent operators to a ca.crt key in <release>-credentials.
That Secret is rendered by this chart from .Values.users and carries
password entries only, so there is nothing in it to verify a server
certificate against.
The anchor a tenant can reach is the key-free <release>.tenant-ca that
the CA extraction controller publishes from the sentinel this chart
renders, delivered through the tenantsecrets API.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
An in-place RestoreJob (targetApplicationRef omitted) re-bootstraps the
Postgres app under its own name. On stock v1.6.2 it never comes back up,
for two independent reasons in the same restore patch - both surface only
in place, because a to-copy restore targets a different app.
1. Archive-vs-recovery serverName collision. The driver set the restored
cluster's WAL-archive serverName equal to the recovery source
(bootstrap.serverName = bootstrap.oldName = sourceServerName) while the
chart pinned the archiver serverName to the release name. In place those
coincide, so the cluster archived onto the source's own WAL prefix and
barman-cloud-check-wal-archive refused to start ("Expected empty
archive"). Fixed by giving the restored cluster a fresh
bootstrap.newServerName, keyed by the RestoreJob UID (unique per
restore, stable across reconciles); the chart archives there while
externalClusters still recovers from bootstrap.serverName. This is what
the manual CNPG strategies hit (serverName defaults to the cluster
name); the cozy-default flow dodged it (serverName is <ns>-<app>).
2. useSystemBucket guard. The restore populates explicit S3 coordinates
(destinationPath / s3CredentialsSecret / endpointCA) but left
backup.useSystemBucket=true, so the chart's "bootstrap.enabled is
incompatible with useSystemBucket" guard failed the HelmRelease upgrade
and the cluster never re-rendered. This is what the cozy-default
(system-bucket) flow hits on an in-place restore. Fixed by clearing
useSystemBucket in the restore patch, since the app is now on the
explicit-creds path. The driver's app view (postgresapp.Backup) gains
the field, without omitempty so a false survives the merge patch.
Both are exercised only by an in-place restore; the postgres backup e2e
drove only to-copy, so neither surfaced in CI. A step 50 in-place restore
now guards reason 1 (manual-strategy flow); reason 2 is covered by a Go
unit assertion (a system-bucket in-place e2e needs the platform bucket in
CI and is left as a follow-up).
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The README told a tenant to fetch <release>.tenant-ca, but the release
name carries the postgres- prefix the application definition applies, so
the object is postgres-<name>.tenant-ca. Following the old text returned
NotFound.
Also drop two comments promising a companion ValidatingAdmissionPolicy
that pins sentinel writes to helm-controller. No such policy ships: the
proposal for it was rejected because authorization by username duplicates
RBAC in a more brittle form. The RBAC boundary is the whole control.
Signed-off-by: Aleksei Sviridkin <f@lex.la>
## What this PR does
Adds an engine-agnostic controller that publishes a key-free
`<release>.tenant-ca` Secret (only `ca.crt`) for a managed application,
so a tenant can verify the application's TLS without ever seeing a
private key. The source is declared, not guessed: a chart renders a
namespaced `TenantProjection` sentinel (group `internal.cozystack.io`)
naming the CA Secret to lift; the controller watches the sentinel,
extracts `ca.crt`, and writes the projection owner-referenced to the
sentinel, so garbage collection is native. The projection carries the
`internal.cozystack.io/tenant-ca` label the lineage webhook turns into
tenant visibility. Postgres is the first consumer.
The trust boundary is RBAC: no tenant role grants any verb on
`internal.cozystack.io`, so a tenant cannot forge a sentinel. A
companion `ValidatingAdmissionPolicy` that pins the writer to
helm-controller ships separately as defence in depth.
This is the controller-and-CRD slice, split out from #3299 for focused
review. It is the only piece required for the feature to work; the
render-time helper, the writer policy, the capability-gate, and the
release-prefix validation land as their own PRs.
### Downstream repositories
Walked the trigger map against the diff. This adds an internal CRD and
controller and a sentinel to the Postgres chart; no app is added or
renamed and no `values.schema.json` changes, so the typed provider and
Ansible are unaffected. The one to confirm is the Postgres README edit —
if the website mirrors it, it needs a refresh.
- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
### Release note
```release-note
feat(cozystack-controller): publish a key-free per-application CA trust anchor (`<release>.tenant-ca`) that tenants read to verify managed-application TLS, declared through a new `TenantProjection` CRD. Postgres is the first consumer.
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added tenant-facing CA certificate projections for PostgreSQL
deployments.
* Tenant secrets now provide only the CA certificate, excluding private
keys.
* Added support for configuring and monitoring tenant CA projections.
* **Documentation**
* Updated PostgreSQL TLS instructions to retrieve certificates through
tenant secrets.
* **Bug Fixes**
* Improved protection against invalid certificates, secret collisions,
and accidental exposure of private key material.
* Added validation to ensure projected certificates remain synchronized
with their sources.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
helm-unittest coverage for the S3 request-checksum fix (was mutation-negative
before): assert spec.instanceSidecarConfiguration.env pins
AWS_REQUEST_CHECKSUM_CALCULATION=when_required on all rendered barman-cloud
ObjectStores — postgres backup + bootstrap-recovery
(packages/apps/postgres/tests/backup_storage_test.yaml) and the Keycloak DB
(new packages/system/keycloak/tests/db_backup_test.yaml). Verified by mutation:
reverting the template change now fails these assertions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
Address review on the S3 request-checksum fix:
- Extend the fix to the platform-managed useSystemBucket=true flow, whose
ObjectStore is built in Go by applyClusterPluginBackup (SSA-applied), not
chart-rendered. Add InstanceSidecarConfiguration to cnpgtypes.ObjectStoreSpec
and set AWS_REQUEST_CHECKSUM_CALCULATION=when_required on it via
barmanSidecarConfiguration(). The platform's own default system bucket is
SeaweedFS, a non-AWS S3 gateway of the same class as Ceph RGW. Unit test in
cnpgstrategy_controller_test.go asserts the applied ObjectStore carries it.
- Factor the duplicated instanceSidecarConfiguration block (3x across two
charts) into a shared cozy-lib helper (cozy-lib.barman.checksumSidecarConfiguration).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
The barman-cloud plugin's sidecar uploads via boto3. Since botocore ~1.36
(early 2025) the default RequestChecksumCalculation is when_supported, which
attaches a flexible checksum to every PutObject. Non-AWS S3-compatible
backends (Ceph RADOS Gateway, some MinIO / Cloudflare R2 builds) reject the
accompanying header with "InvalidArgument: x-amz-content-sha256 must be
UNSIGNED-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD or a valid sha256
value", so every backup/WAL-archive upload fails against them.
Pin the sidecar to when_required via the ObjectStore's
spec.instanceSidecarConfiguration.env (botocore honors the
AWS_REQUEST_CHECKSUM_CALCULATION env var) on both the keycloak system DB and
the postgres app (backup + recovery ObjectStores). when_required is also
accepted by AWS S3 on a plain PutObject, so it is a safe default everywhere.
This mirrors the etcd-operator fix (cozystack/etcd-operator#342, released in
v0.5.3), which applied the same WhenRequired policy to its aws-sdk-go-v2 S3
client for the same Ceph RGW backend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
CloudNativePG stores its self-signed CA in <release>-ca alongside the
private key and offers no way to label it, so a tenant cannot be handed
the trust anchor without also being handed the key. Render a
TenantProjection that lifts ca.crt alone into the key-free
<release>.tenant-ca Secret, and select that label from the postgres
tenant-resource definition so the projection is served through the
tenantsecrets API. Document the new retrieval path in the chart README.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The db.yaml plugins-block comment still carried the over-generalized
claim that the pinned postgresql image variants no longer ship the
barman-cloud-* binaries; mirror the scoped wording from
docs/operations/backup-classes.md (only the standard variant keycloak-db
pins lacks them; this chart's bare system-flavor tags still ship them
but the flavor is deprecated upstream).
After the rebase onto main this PR no longer touches linstor-scheduler:
main re-vendored it to upstream chart 0.3.1 / extender v0.3.6, which
keeps the admission webhook and stops stripping unknown pod fields
(including initContainers[].restartPolicy the barman sidecar needs). The
upgrade note claiming the webhook is removed and locality scheduling
becomes advisory is rewritten as a hold-back warning.
Also: afterwards -> afterward (American English, review nit).
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
make -C packages/apps/postgres generate re-pads the Bootstrap parameters
table for the new bootstrap.serverName row; the committed README still
carried the old padding and kept the pre-commit generate check red.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Two defects of the same class — the WAL-archive S3 prefix (serverName)
changing or defaulting differently between components:
Recovery default regression (review blocker): native CNPG defaulted an
absent externalClusters serverName to the entry name (= bootstrap.oldName),
but the barman-cloud plugin defaults it to the NEW cluster's name, so a
legacy-flow restore with bootstrap.serverName unset would read the wrong
S3 prefix (hard failure, or silently restoring another cluster's data).
The chart now always emits serverName, defaulting to bootstrap.oldName.
Live-cluster serverName flip (found by the gated postgres backup e2e on a
real cluster): the chart renders spec.plugins without serverName
(effective prefix = cluster name) while a strategy template may name a
different one; the driver's ForceOwnership patch then flipped the prefix
mid-stream, splitting the WAL archive across two prefixes — the WAL
holding the backup's start was lost between them and the restore failed
with "WAL not found". The driver now preserves the live Cluster's
effective serverName (explicit parameter, or the cluster name when the
plugin is attached without one) and only applies the strategy's value to
a Cluster with no plugin attached; both charts pin serverName explicitly,
and the example strategy no longer sets one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Review follow-up (cozystack#3300, non-blocking): the barman-cloud ObjectStore CRD
validates spec.retentionPolicy against ^[1-9][0-9]*[dwm]$, so an operator who
explicitly sets backup.retentionPolicy="" would render retentionPolicy: "" and
the API server would reject the ObjectStore, breaking backup provisioning. Guard
the field in the apps/postgres and keycloak chart ObjectStores with
{{- if .Values.backup.retentionPolicy }} (the Go driver path is already safe via
omitempty). Add a helm-unittest asserting an empty retentionPolicy is omitted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Review follow-up (cozystack#3300): the apps/postgres user-facing docs still
described the removed native spec.backup.barmanObjectStore / archive_command
model. Update the values.yaml @field/@typedef/@param annotations (useSystemBucket,
schedule, S3CredentialsSecret, EndpointCA, endpointCA, the @param backup note,
bootstrap.serverName) and the hand-written README Operations prose to the
barman-cloud plugin + ObjectStore model, then regenerate values.schema.json, the
README parameters table, the api types, and the postgres-rd ApplicationDefinition
schema (which feed the dashboard form help + cozyrd schema). Field names,
defaults, and the storageClass immutability marker are unchanged; only backup/
bootstrap description text changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Review follow-ups for the CNPG barman-cloud plugin migration (cozystack#3300):
- docs/operations/backup-classes.md: the 'Upgrade notes from chart-managed
backups' section still described the removed native spec.backup.barmanObjectStore
behavior. Rewrite it for the plugin flow (chart renders spec.plugins + a
barmancloud.cnpg.io/ObjectStore; the driver SSA-applies an ObjectStore and
patches spec.plugins), and document the real upgrade hazard: the
plugin-barman-cloud operator + cert-manager must be present in
cozy-postgres-operator or the Cluster references an unregistered plugin and
backups/WAL archiving stall.
- apps/postgres backup_storage_test.yaml: add a positive bootstrap-recovery
rendering test. Asserts externalClusters[].plugin references
barman-cloud.cloudnative-pg.io with barmanObjectName=<release>-recovery and
serverName on the plugin parameter, and that the -recovery ObjectStore carries
spec.configuration but NOT serverName (the plugin forbids it there). Previously
the recovery path had only fail-fast guard tests, no positive coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Reconcile the Chainsaw e2e migration with ~500 commits of main drift:
- keep main's run-kubernetes.sh helpers (kubectl_wait_retry, tenant
drain, LINSTOR pool-free wait, SC-fallback probe) under the branch's
_lib/ rename with the Chainsaw finally/snapshot deltas re-applied
- resolve modify/delete on 10 drifted hack/e2e-apps/*.bats as deleted;
their post-branch assertion drift is re-ported into the Chainsaw
suites in follow-up commits
- drop the 4 new main-side bats (kubernetes-oidc-*, securitygroup,
serviceexposure) from _lib/ relocation; ported as Chainsaw suites in
follow-up commits
- docs/agents/e2e-testing.md: merge main's plan-job TIA wording with
the Chainsaw suite phrasing
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback on #3069:
- ttlSecondsAfterFinished 120 -> 3600 so a failed init Job leaves a 1h
debugging window before the TTL controller collects it (the TTL also
reaps failed Jobs, not just successful ones).
- Load templates/init-script.yaml in the unittest suite: the Job's
checksum/config annotation includes that template, so rendering
init-job.yaml in isolation failed in CI. Select the Job via
documentSelector and assert the 3600 TTL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Алексей Артамонов <alexeyartamonov1987@gmail.com>