Commit Graph
7851 Commits
Author SHA1 Message Date
Andrey Kolkov 6f3d26c491 feat(backups): tell on the BackupJob why a Redis artifact was not recorded
A Backup without status.artifact looked the same whether a custom
strategy wrote elsewhere, the credentials Secret was unusable or the
endpoint CA was gone; only the controller log told them apart. The
probe now returns why it gave up, and the controller records it as a
Normal ArtifactNotMeasured Event on the BackupJob. The Event lives in
the application namespace, so it carries a fixed reason only; the raw
error, which may name the platform's endpoint and bucket, stays in the
log.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 11:21:46 +07:00
Andrey Kolkov d61a5054a1 fix(backups): refuse to probe a Redis snapshot behind a CA that is gone
The default strategy mounts the s3-ca volume only when the admin
configured an endpoint CA, and the script then trusts that CA alone. When
the volume yields no CA by the time the probe runs, because the Secret
was deleted or emptied after the Job or the volume has another source,
the probe used to fall back to the system roots, trusting more than the
Job may have. It now leaves the artifact unset instead, with or without
skip-verify.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 11:21:46 +07:00
Andrey Kolkov 43bed8f6d9 fix(backups): keep the Redis probe verifying TLS unless every container skips
The probe read S3_INSECURE_SKIP_VERIFY from any container of the backup
Job, so a custom strategy whose sidecar sets it while the uploading
container verifies made the probe skip verification the upload did
not. The probe cannot tell which container uploaded, so it now skips
only when every container sets the variable to true, the last entry
winning as in the kubelet.

An s3-ca volume of a source other than a Secret yields no CA the probe
can read; it is now treated like a CA Secret that is gone, so it never
turns into skip-verify.

Tests pin the guards that decide the probe's trust: the Redis CA volume
mounted next to the S3 one, a decoy variable set to true, a CA that did
not sign the endpoint, and the AlreadyExists read-back past the cache.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 11:01:18 +07:00
Andrey Kolkov e03986e8d0 fix(backups): treat an S3 object without a reported size as unmeasured
minio-go returns size -1 when a HEAD answer carries no Content-Length,
which would land in Backup.status.artifact.sizeBytes as -1. The probe
now reports such an object as unmeasured, so the field stays unset.

The shared client's trust settings also gain a test against an endpoint
whose certificate the configured CA did not sign, with the CA alone and
on top of the system roots: nothing pinned that a configured CA is
actually enforced.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 11:01:18 +07:00
Andrey Kolkov 631f6cfc0d fix(backups): mirror the Redis script's CA trust exactly
The script passes the endpoint CA as curl --cacert, which replaces the
default trust store, while the probe trusted the system roots on top of
it. Trust only the CA, as the script does.

A mounted CA Secret that is still there but no longer carries ca.crt or
tls.crt let the probe skip TLS verification when the strategy opts into
it, although the Job may have verified against a key since removed.
Treat a volume that yields no CA now like a vanished Secret and refuse
to skip verification in its place.

Pin the probe timeout and the reconcile's retry of an unreadable Backup
for Redis with tests.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 10:52:45 +07:00
Andrey Kolkov 50b1a2e5ee test(backups): pin the Redis retry check
Cover the two retry-check paths the Redis driver shares with MariaDB and
CNPG: a failed existence check creates the Backup without probing the
bucket, and an existing Backup that cannot be read back is returned as
errBackupNotReadBack so the reconcile is retried.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 10:52:45 +07:00
Andrey Kolkov bfea468b6c fix(backups): never skip TLS verification in place of a vanished CA
The Redis probe reads the CA Secret when it runs, not when the backup
Job did. If the pod mounted a CA Secret that has since been deleted and
the strategy also opts into S3_INSECURE_SKIP_VERIFY, the probe skipped
verification against an endpoint the Job may have verified. Refuse to
probe in that case. Without skip-verify a missing CA Secret still falls
back to the system roots, as the script does.

The endpoint comment claimed a bare host; it can carry a path, which the
shared client now refuses.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 10:52:45 +07:00
Andrey Kolkov 2ff547348d fix(backups): probe the Redis snapshot with the trust its Job used
The default Redis strategy script verifies the S3 endpoint against
ca.crt, then tls.crt, from the Secret mounted as the s3-ca volume
(backupStorage.endpointCASecretName), and only without either honours
S3_INSECURE_SKIP_VERIFY=true. The probe always verified against the
system roots, so on a cluster with a private CA, such as a self-signed
SeaweedFS, every Redis and Valkey backup succeeded with no artifact.

Read the same settings from the pod spec of the completed backup Job, in
the order the script applies them, and pass them to the S3 connection.
The shared transport gains an InsecureSkipVerify switch that is set only
from that explicit opt-in.

The projector always stores the endpoint as a bare host, so the branch
that parsed a scheme out of it was unreachable; drop it and its test.
Cover the endpoint and access-key terms of the Secret completeness check.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 10:52:45 +07:00
Andrey Kolkov c8cdefc665 feat(backups): record the RDB snapshot location and size for Redis backups
Backups made by the Redis strategy, which also serves Valkey, carried no
Backup.status.artifact at all: neither a URI nor a size. The snapshot is
one object per backup, <namespace>/<app>/<backup>.rdb, whose key the
driver already records, and the default strategy uploads it with the S3
coordinates of the backup credentials Secret the controller projects
into the application namespace.

On completion the driver now stats that object with the same coordinates
and records its s3:// URI and size. The artifact is set only when the
object is found, so a custom strategy that writes elsewhere gets no URI
pointing at a missing object. A Backup that already exists is returned
without probing, as for the MariaDB and CNPG drivers.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-10 10:52:45 +07:00
Aleksei Sviridkin 5e9c392352 feat(api): add the reservation evaluator for tenant quotas (#4983)
## What this PR does

First phase of the accepted [tenant quotas as reservation
limits](https://github.com/cozystack/community/pull/48) design: a
standalone, importable `pkg/reservation` package that computes what an
application consumes from a tenant's budget from its declared values.
Nothing calls it yet; the usage oracle, the generalized admission gate
and the per-kind declarations come in the following phases.

Today a tenant quota is declared in instance-type units but enforced
against `ResourceQuota.status.used`, which counts virt-launcher pods
with their additive virtualization overhead and drifts without
recomputation. The design makes the declared reservation the accounting
authority instead, and this package is the evaluator both the apiserver
and the controller will read.

- **Declarations are data, not Go.** A kind describes its consumption as
CEL over its values (`exempt`, ordered `let` bindings, one expression
per quota key), so the evaluator holds no per-kind knowledge and a kind
from an out-of-tree catalog participates without a change here. The
`Declaration` type is ready to be embedded in
`ApplicationDefinitionSpec` in phase 2. Environment functions are
`instanceType(name)`, `preset(name)` and `quantity(x)`, on top of the
Kubernetes CEL quantity library; scaling is `quantity.mul(n)`, because
the standard `_*_` cannot take a quantity overload.
- **Instance types resolve to scalars keyed by class**, read from the
type's spec and never from its name: `cpu`, `dedicated-cpu` (guest cores
plus the isolated emulator thread, the extra parity core when the type
itself carries the `EmulatorThreadCompleteToEvenParity` annotation, and
a supplemental I/O pool), `memory`, `hugepages-<size>`,
`overcommitted-memory`. The test pins the classification against every
`VirtualMachineClusterInstancetype` shipped in
`packages/system/kubevirt-instancetypes`.
- **Fail closed.** An unknown instance type or preset, a value of the
wrong type, an unguarded absent key, an unparseable quantity or a
negative figure is an error, and values are checked against the kind's
schema before defaulting, which would otherwise replace a mistyped value
with its default, never a smaller reservation. A definition that
declares neither its consumption nor an exemption yields
`ErrNoDeclaration`.
- **Stored values are defaulted first.** The aggregated apiserver
persists a tenant's spec without its schema defaults, so an evaluator
reading them raw would count zero for `kubernetes-nodes.maxReplicas` on
every pool created with defaults. The structural-schema defaulting moves
from `pkg/registry/apps/application` into `pkg/apis/apps/defaulting` so
the registry and the evaluator apply the same defaults; the registry now
only delegates to it, with no behaviour change.
- **Aggregation** sums reservations per namespace and returns the
applications that could not be evaluated separately, so a caller never
mistakes an incomplete sum for a complete one.

The evaluator tests run against the shipped OpenAPI schemas of
`postgres`, `vm-instance` and `kubernetes-nodes`: the cozy-lib per-key
preset fill, a `{cpu: 4, sockets: 2}` block reserving 8 vCPUs, the
autoscaling ceiling, `external: true` reserving a load balancer, and a
ComputePlane-shaped `KubernetesNodes` release carrying only `{roles,
minReplicas}` reserving exactly what the same pool with its defaults
written out reserves.

Left for the next phases on purpose: where presets are resolved from
(the design's direction is platform-shipped environment objects; here
they come through the `Environment` interface), the
`ApplicationDefinition` field itself, and any consumer.

### Screenshots

Not a UI change.

### Downstream repositories

The diff adds two Go packages and moves the defaulting helpers without
changing behaviour. It touches no package under `packages/`, no
`values.yaml`, no CRD and no `ApplicationDefinition` field, so none of
the triggers in the map 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
feat(api): add the reservation evaluator that computes an application's tenant-quota consumption from its declared values. Internal only, with no consumer yet and no change in behaviour.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

- **New Features**
- Added reservation evaluation for application resources, including CPU,
memory, hugepages, and overcommitted memory.
- Added support for instance types, resource presets, schema-based
defaults, and CEL expressions.
- Added namespace-level reservation aggregation with clear reporting for
applications that cannot be evaluated.
- Added validation for invalid, negative, unsupported, or excessive
resource values.
- Added classification of KubeVirt instance types, including dedicated
CPUs, emulator threads, supplemental I/O threads, and hugepages.

- **Bug Fixes**
- Schema defaults are applied consistently to nested application values.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-10 03:26:01 +03:00
Aleksei Sviridkin b53ca1fede fix(postgres): refuse system databases and never drop them (#5155)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

`databases` accepts `postgres`, `template0` and `template1`, and the
init job treats them like application databases: it comments them
`database managed by helm`, creates `<db>_admin` and `<db>_readonly`,
makes `<db>_admin` the owner of the database and its schemas, and
installs the `trigger_auto_grant` event trigger. When the entry later
leaves values, the delete pass runs `DROP DATABASE` on it, which can
only fail:

- `template1`, `template0`: `cannot drop a template database`;
- `postgres`: `cannot drop the currently open database`, since the job
is connected to it.

The init job then fails on every upgrade, so the HelmRelease sits in
`UpgradeFailed` with `post-upgrade hooks failed`, and putting the entry
back does not help. `template1` also hands the takeover (owner of
`public`, default privileges, trigger) to every database created
afterwards. Once its roles are dropped by hand, the trigger left behind
names a role that no longer exists, so `CREATE SCHEMA` fails in every
new database, even as superuser.

This PR:

- **Refuses** the three names at render with a message that says what to
do. `cozyvalues-gen` has no way to constrain the keys of a
`map[string]Database`, so the refusal sits next to the existing
database-name guard, like the `users.postgres` refusal. The field
description says it too.
- **Never selects them for `DROP DATABASE`.** The delete pass excludes
them whatever values say.
- **Hands a taken-over system database back.** In a new `release system
databases` pass before the delete pass, for each of the three still
commented `database managed by helm`: drop the trigger and
`auto_grant_schema_privileges()`, `REASSIGN OWNED` / `DROP OWNED` for
its two roles in that database, give `public` back to
`pg_database_owner` (PostgreSQL 15+), and clear the comment. The
existing orphaned-roles pass then drops the roles; it never visits
`template1`, hence the per-database step. `template0` accepts no
connection and the job that commented it never got further, so only its
comment is cleared.

**Upgrade note.** A release that declares one of the three names fails
to render until the entry is removed. Removing it is now safe: the next
run releases the database instead of dropping it.

**Testing**

- `tests/system_databases_test.yaml`: 5 of its 6 cases fail on `main`
without the change. `helm unittest packages/apps/postgres`: 215 passed.
- I replayed the scenario against a `postgres:17` container, running the
rendered `init.sh` with `PGUSER=postgres`:
1. The `main` script with `databases: {appdb, template1, postgres}`
reproduces the takeover.
2. The `main` script with only `appdb` then fails with `cannot drop the
currently open database`.
3. This branch's script on that same state exits 0. It releases both
databases (owner `postgres`, no comment, no event trigger, `public`
owned by `pg_database_owner`) and drops `postgres_admin`,
`postgres_readonly`, `template1_admin` and `template1_readonly`. A
database created afterwards gets no trigger, and `CREATE SCHEMA`
succeeds in it.
  4. A second run is a no-op.
5. A `template0` commented and given roles by hand is released the same
way.

Open PR #5091 touches the same script (stable database order); the hunks
do not overlap.

Backport: the automatic cherry-pick onto `release-1.6` conflicts in
`init-script.yaml` and the generated cozyrd, so the backport to
`release-1.6` is done by hand in #5157 (same SQL, replayed against that
branch's script). `release-1.7` keeps `kind/backport`.


### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [x] No downstream repository is affected by this change (walked the
map: a template guard, the init script, a test suite and one field
description regenerated into README, schema, Go types and cozyrd; no
field, default, enum or name 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

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(postgres): databases now refuses the PostgreSQL system databases postgres, template0 and template1, and the init job never runs DROP DATABASE on them. A system database a previous version took over is handed back to the superuser on the next upgrade (event trigger removed, ownership and public schema restored, its <db>_admin and <db>_readonly roles dropped). A release that declares one of them must remove the entry before upgrading.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Behavior Changes**
* PostgreSQL system databases (`postgres`, `template0`, and `template1`)
are refused as managed application databases.
* Previously managed system databases are released during cleanup and
excluded from database deletion. Cleanup restores eligible ownership and
security settings and clears system database comments.
* **Documentation**
* Updated PostgreSQL configuration guidance to clarify that these system
database names are refused. Names that merely contain these strings
remain accepted.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 21:09:23 +03:00
scooby87 10e0ef9ece fix(postgres): fail a backup job as soon as its cnpg Backup fails (#5211)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

Part of #5201. The remaining part, a bound on a Backup that stays
`pending`, is in #5209.

A Postgres BackupJob whose `cnpg.io/Backup` had failed stayed `Running`
for 30 minutes with reason `CNPGBackupTransientFailure`, on the premise
that CloudNativePG may move a Backup out of the failed phase. It does
not: in CloudNativePG 1.30.0 the backup controller returns early on any
Backup already `failed` or `completed`, so the wait only delayed the
verdict. The BackupJob now fails as soon as the Backup is `failed`, with
the error CloudNativePG recorded. When that error is only the plugin's
exit status, the message points to the `plugin-barman-cloud` sidecar
log, where the storage error (`AccessDenied`, a connect timeout) is.

A Backup in any other phase keeps the BackupJob waiting as before, but
the BackupJob now carries a `Ready=False` / `WaitingForCNPGBackup`
condition naming the Backup and its phase instead of no condition at
all. These phases stay uncapped on purpose: CloudNativePG keeps a
healthy Backup `pending` while another backup of the same Cluster runs
and while it restarts one on another instance, so a fixed deadline here
would fail healthy backups, in particular scheduled ones that queue
behind a long previous run. #5209 tracks bounding a Backup that is stuck
rather than queued, together with the Plan concurrency that makes
queueing common.

A table-driven test drives the reconciler through each Backup phase and
asserts the job phase, the message, the persisted condition, that an
unchanged phase does not rewrite the BackupJob status, and that only a
completed Backup produces a Cozystack `Backup`.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [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

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(postgres): a Postgres BackupJob now fails as soon as its CloudNativePG Backup fails, with the recorded error, instead of after 30 minutes, and a BackupJob waiting on a CloudNativePG Backup reports a WaitingForCNPGBackup condition with the Backup's phase.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* CNPG backups that enter a failed state now fail their BackupJob
immediately, with the recorded error shown when available.
* BackupJobs waiting for a CNPG backup to finish remain in a waiting
state, even after the backup deadline; the deadline applies while
waiting for the backup to be created.
* Waiting status now includes a clear reason, and failure messages
provide a sidecar-log hint when relevant.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 18:17:03 +03:00
myasnikovdaniil 91f03dd90e fix(platform): remove the dead registries value and patch-containerd Secret (#5190)
Nothing reads the `patch-containerd` Secret: the kubeadm certs.d mount
it fed is gone, and the v1.6.0 changelog already says the passthrough is
removed. The `registries` value stayed in the Package spec and takes
mirrors that do nothing, which is worse than no knob for someone setting
up an air-gapped cluster. Removed both.

A Package that still sets `registries` keeps installing, the platform
chart has no values schema so the key is ignored. On upgrade the Secret
in `cozy-system` is deleted by the release.

Mirrors for tenant workers are the per-pool `talos.registryMirrors` of
KubernetesNodes.

I checked that nothing is left: `git grep` for `.Values.registries` and
for `patch-containerd` outside the changelogs finds nothing but the
kubernetes README sentence that says the Secret was removed. Full `make
unit-tests` passes.

Fixes https://github.com/cozystack/cozystack/issues/4006


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Configuration Changes**
* The platform chart no longer supports the `registries` configuration
option or generates containerd registry secrets. Mirror endpoint
settings, including credentials and TLS verification options, are no
longer applied through this chart.
* Registry mirrors can be configured per worker pool using
`talos.registryMirrors` on `KubernetesNodes`. Existing packages that set
`registries` continue to install, but the setting is ignored.
* **Documentation**
  * Updated guidance for configuring registry mirrors on worker pools.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 19:36:30 +05:00
Aleksei Sviridkin 63abbc80ee fix(api): read pool usage under the quota key the controller counts (#5184)
## What this PR does

Tenant quota admission does not see how much of an extended resource or
hugepages the parent pool already uses, so a child tenant can be granted
a share the parent has already spent. Admission reads that usage from
`limits.<key>`, but Kubernetes reports it only under `requests.<key>`,
so the value is always zero.

Now each key is read where the quota controller counts it. cpu, memory
and ephemeral-storage still read `limits.*`. The new tests fail on main.

### Screenshots

### Downstream repositories

- [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(api): tenant quota admission now counts the parent pool's usage of extended resources and hugepages, so a child tenant can no longer get a share the parent has already used. After upgrading, updates to existing tenants may be rejected if their quota for these resources exceeds the parent's remaining budget. This includes updates that leave the quota unchanged.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Quota checks now account for existing usage of extended resources,
preventing child requests from exceeding the parent’s remaining quota.
* Requests are allowed when current usage leaves enough quota available,
and denied when usage has consumed the parent’s full quota.
* Resource usage is matched against the appropriate quota category,
including requests for extended resources and hugepages.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 17:23:39 +03:00
Mattia Eleuteri 0a310cc507 fix(api): default into typed nil maps and slices
A nil map or slice held in an interface is not == nil, so the
defaulting walk took it for a present value: writing the first default
into a nil map panicked, and a nil slice kept no default. The registry
always passes a decoded map, but Apply is now a shared entry point and
nothing guarantees that for its other callers.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-11-09 11:36:14 +01:00
Mattia Eleuteri 89aba6e104 feat(api): add the reservation evaluator for tenant quotas
Tenant quotas are declared in instance-type units but enforced against
ResourceQuota.status.used, which counts virt-launcher pods with their
additive virtualization overhead and drifts without recomputation. The
accepted tenant-quota-reservation design makes the declared reservation
the accounting authority instead; this is its first phase, the
evaluator, with no consumer yet.

A kind describes its consumption as CEL over its values, so the
evaluator carries no per-kind knowledge and an out-of-tree kind
participates without a change here. Instance types resolve to scalars
keyed by class, read from the type's spec rather than its name, because
a pinned core, a hugepage and an overcommitted page are not the same
capacity as a shared vCPU or ordinary memory. KubeVirt pins the
emulator thread's parity core per type, from the annotations the type
hands to its VMs, so the classifier reads it there too. Anything that
does not resolve is an error, never a smaller figure.

Stored values carry only the keys a tenant wrote, so the evaluator
defaults them first. The defaulting code moves out of the application
registry into its own package so both read the same defaults. Defaulting
replaces a value of the wrong type with the schema's default, which
would turn a malformed spec into a smaller reservation, so values are
type-checked against the schema before it runs.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-11-09 11:35:53 +01:00
Mattia Eleuteri f003851338 fix(postgres): refuse system databases and never drop them
postgres, template0 and template1 were accepted in databases and handled
like an application database. The init job made <db>_admin their owner,
installed the auto-grant event trigger in them and, once the entry left
values, ran DROP DATABASE on them. That DROP can only fail (template
databases cannot be dropped, and the job is connected to postgres), so
every later upgrade failed in the init job and the release stayed in
UpgradeFailed even after the entry was put back. template1 also passed
the takeover on to every database created from it afterwards, where the
trigger, naming a role about to be dropped, makes CREATE SCHEMA fail.

A release a previous version let take one of them over has to be
repaired without dropping it, and without touching what this chart did
not create: a role or owner that merely carries an expected name may
belong to a tenant or an administrator, and a role comment can be set
by any CREATEROLE holder, so only the cozystack:managed mark is trusted.
What the marked roles own goes to cozystack:orphaned rather than to the
superuser, since a tenant's SECURITY DEFINER function would otherwise
run with superuser rights. A failed lookup of the databases cloned from
template1 has to stop the run before template1 stops being selected, or
those clones would never be repaired.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-11-09 11:18:27 +01:00
Aleksei Sviridkin 1f35c26ece fix(lineage, kubernetes): resolve CAPK node VMs to their cluster (#5124)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

The virt-launcher pods of Kubernetes worker nodes are never marked as
managed by the lineage webhook, so anything that relies on the webhook
having resolved them misses them. The visible symptom: a SecurityGroup
attached to a `Kubernetes` application labels the control-plane pods but
never the worker nodes, because the securitygroup-controller caches only
pods carrying `internal.cozystack.io/managed-by-cozystack=true`.

**Root cause.** The webhook is not skipped for these pods (they do not
carry the managed-by label, so the `objectSelector` lets them through);
it runs and fails to resolve their owner chain. The walk goes Pod →
VirtualMachineInstance → VirtualMachine and stops there: the Cluster API
KubeVirt provider creates the VirtualMachine with no ownerReference and
links it to its KubevirtMachine through the
`capk.cluster.x-k8s.io/kubevirt-machine-{name,namespace}` labels only.
With no ownerReference, no HelmRelease kind and no
`helm.toolkit.fluxcd.io/name` label on the VM, `WalkOwnershipGraph`
returns no ancestor, `computeLabels` returns nothing and the pod is
admitted unmanaged. The
`apps.cozystack.io/application.{group,kind,name}` labels the pods do
carry come from the `KubevirtMachineTemplate` in
`packages/apps/kubernetes-nodes/templates/nodegroup.yaml`, not from the
webhook.

**Measured on a v1.6.5 cluster.** Every Kubernetes cluster on it showed
the same shape (three clusters across two tenants). For one worker node:
the pod carries the three lineage labels but not `managed-by-cozystack`;
the pod is owned by its VMI and the VMI by its VM; the VM has no
ownerReferences, and its labels include
`capk.cluster.x-k8s.io/kubevirt-machine-name` and
`capk.cluster.x-k8s.io/kubevirt-machine-namespace` pointing at a
KubevirtMachine in the same namespace. From there ownerReferences run
KubevirtMachine → Machine → MachineSet → MachineDeployment → Cluster,
and the CAPI Cluster carries `helm.toolkit.fluxcd.io/name:
kubernetes-<name>`. The bridge is one hop. Setting
`managed-by-cozystack=true` by hand on a worker's virt-launcher pod made
the securitygroup-controller stamp the membership label within ten
seconds.

**Fix.** `WalkOwnershipGraph` now follows a VirtualMachine without
ownerReferences to the KubevirtMachine named by its CAPK labels, and
keeps walking from there. Node pods then resolve to the parent
`Kubernetes` application, which is the attribution the machine template
already hard-codes, and they get `managed-by-cozystack` like every other
managed pod. The bridge is only taken when the label names the VM's own
namespace: CAPK can target an external infrastructure cluster, and the
walk must never attribute an object across namespaces. The webhook's
ClusterRole already grants `get` on every resource, so no RBAC change is
needed.

**Why at the source rather than in the consumer.** Relaxing the
securitygroup-controller's pod cache to
`apps.cozystack.io/application.kind` Exists would also have fixed the
symptom, but it would leave every other consumer of the managed-by label
blind to node pods, and it would rest membership on labels the webhook
never vouched for. Stamping `managed-by-cozystack` from the machine
template was ruled out too: the `objectSelector` would then skip these
pods entirely, and they would lose the scheduling-class injection the
webhook performs on every pod.

**Blast radius.** Only objects below a CAPK VM change: the VMI, the
virt-launcher pods and the PVCs of the VM's `dataVolumeTemplates` now
get the lineage labels of their Kubernetes application. None of them
match a resource selector of the `Kubernetes` ApplicationDefinition, so
they get `tenantresource=false`. Nothing selects PVCs on
`application.kind=Kubernetes` (the Velero strategy selects `VMDisk`).
The `-userdata` cloud-init Secret CAPK creates is not affected either
way: at the pinned provider commit, `reconcileKubevirtBootstrapSecret`
creates it with the bootstrap Secret's labels and no ownerReferences, so
the walk reaches neither the VM nor the KubevirtMachine from it, before
or after this change. Existing node pods pick the labels up on their
next admission (recreation, live migration, or any update).

**Tests.** `pkg/lineage/capk_test.go` builds the observed chain in a
fake dynamic client and checks that a worker's virt-launcher pod
resolves to `Kubernetes/cluster-a`, that a VM pointing at a
KubevirtMachine in another namespace is not followed, that a VM without
CAPK labels still has no ancestor, and, in a table of negative cases,
that the CAPK labels are ignored on any object other than a
`kubevirt.io/v1` VirtualMachine (another kind of the `kubevirt.io`
group, a VirtualMachineInstance, a VirtualMachine of another group or
version). Weakening the kind and API version guard to match on either
field alone fails all four cases. With the bridge disabled, the first
test fails. No chainsaw case: the only suites that bring up worker VMs
are `kubernetes-latest` and `kubernetes-previous`, whose shared runner
script is already over its time budget, and the unit test covers the
walk exactly as it runs in the webhook.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [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

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(lineage, kubernetes): the virt-launcher pods of Kubernetes worker nodes are now resolved to their Kubernetes application and marked as managed by Cozystack, so a SecurityGroup attached to a Kubernetes cluster now applies to its worker nodes, not only to its control plane.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Lineage results now connect supported virtual machines to their
associated machine and Kubernetes application when the required
association labels are present.
* **Bug Fixes**
* Association tracing is limited to supported virtual machine resources
and machines in the same namespace. Unlabelled standalone virtual
machines and machines in another namespace do not gain ancestors through
this association.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:28:36 +03:00
Aleksei Sviridkin da7f2c2f50 fix(tests): keep two unit tests green after the release image restamp (#5180)
## What this PR does

Same change as #5179, on `main`. Both tests pass here today, because
`main` still carries the pre-release image pins; they fail on every
release line as soon as the first rc is cut, since the `Prepare release`
commit rewrites `packages/*/values.yaml` and two unit tests read exactly
the fields it rewrites. On `release-1.7` this has kept `Unit &
controller tests` red on every PR since v1.7.0-rc.3
([job](https://github.com/cozystack/cozystack/actions/runs/41162009892/job/124421449910)
on #5172), and `release-1.8` would repeat it.

| test | reads | before the release build | after (release-1.7 today) |
|---|---|---|---|
| `hack/cni-plugins-staging-contract.bats` "the kube-ovn release the CNI
plugin pin was checked against is the one vendored" |
`.global.images.kubeovn.tag` in `packages/system/kubeovn/values.yaml` |
`v1.15.26@sha256:…` | `v1.7.0@sha256:…` |
| `packages/system/monitoring/tests/vtraces_collector_test.yaml` "pins
the collector image through the cozy-lib.image helper" |
`tracingCollector.image` |
`docker.io/otel/opentelemetry-collector-contrib:0.136.0@sha256:…` |
`ghcr.io/cozystack/cozystack/otelcol-tracing:v0.136.0-cozystack.0@sha256:…`
|

Changes:

- The bats test takes the vendored kube-ovn release from `appVersion` in
`packages/system/kubeovn/charts/kube-ovn/Chart.yaml`. That is the value
`packages/system/kubeovn/Makefile` builds the image from, and a release
build leaves it alone. The hint printed on a mismatch names the new
file.
- The helm unittest case no longer names the image at all. It sets
`_cluster.images-registry` and asserts that the registry lands in front
of the rendered image and that the image stays digest-pinned (the
`grafana_upstream_image_plugins_test.yaml` pattern). Removing the
`cozy-lib.image` call from the template makes it fail; the old regex did
not.

Same single commit as #5179; the only difference is the unrelated `DOC=`
line next to the edit, kept as `main` has it.

Verified locally on this branch: `bats
hack/cni-plugins-staging-contract.bats` 10/10, `helm unittest
packages/system/monitoring -f tests/vtraces_collector_test.yaml` 28/28.

Refs #5178 (the release-1.7 report), #5179, #5172.

### Downstream repositories

- [x] No downstream repository is affected by this change

### Release note

```release-note
NONE
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Tests**
* Version validation now compares the checked version with the chart’s
application version and reports missing or mismatched versions with the
chart path.
* Monitoring image checks now verify use of the configured mirror
registry and require a 64-character lowercase SHA-256 digest, without
requiring a specific image name.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:19:21 +03:00
Aleksei Sviridkin 23461e065d fix(redis): run one Sentinel when Redis runs a single pod (#5151)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

The chart pins `spec.sentinel.replicas: 3` whatever `replicas` says, so
an instance created with `replicas: 1` runs one Redis pod and three
Sentinel pods that have nothing to fail over to. This makes the Sentinel
count follow the data pods: one Sentinel when `replicas` is 1, three
otherwise.

The operator cannot run zero Sentinels (freshworks `Validate()` resets
`sentinel.replicas <= 0` to 3), and it needs at least one to locate the
master. With one Sentinel its quorum is `replicas/2+1` = 1, so the
single Sentinel works on its own.

The operator also gives the Sentinel Deployment a PodDisruptionBudget
with `minAvailable: 1` whenever Redis has two replicas or fewer
(`ensurePodDisruptionBudget` in
`operator/redisfailover/service/client.go`). On a single Sentinel that
budget refuses every eviction and would block node drains, so the chart
sets `sentinel.disablePodDisruptionBudget: true` in that case only. The
Sentinel `WorkloadMonitor` now expects the rendered count instead of a
fixed 3/2.

The operator only stops ensuring that PDB once the flag is set; it never
deletes the one it created while three Sentinels ran
(`EnsureSentinelDeployment` in freshworks v3.3.5). An existing instance
scaled to one replica would keep it, so
`templates/hooks/sentinel-pdb.yaml` adds a post-upgrade hook that
deletes `rfs-<release>`, with a Role scoped to that one name. It renders
on upgrade only, since a fresh one-replica install never gets that PDB.

From `replicas: 2` up nothing changes. Scaling an existing instance
between 1 and 2 replicas scales the Sentinel Deployment between 1 and 3
accordingly.

Not in scope: with `replicas: 1` the Redis data pod keeps its own
operator PDB (`minAvailable: 1`), which already blocks drains of its
node today; that is unchanged here.

Tests: `tests/sentinel_replicas_test.yaml` covers 1, 2, 3 and the
default, the PDB switch, and the `WorkloadMonitor`. Three of its cases
fail on `main` without the template change.
`tests/sentinel_pdb_hook_test.yaml` covers the hook. `helm unittest
packages/apps/redis`: 79 passed. Open PR #4990 also edits
`redisfailover.yaml` (Sentinel spreading); the two touch different keys.

Backport: `release-1.7` ships the same freshworks operator and CRD, so
`kind/backport` applies. `release-1.6` is not backportable as is: it
still runs spotahome `v1.3.0-rc1`, whose vendored CRD (chart 3.3.0) does
not declare `sentinel.disablePodDisruptionBudget`, and the operator's
CRDs are installed with Flux's default `crds: Create` policy, so they
are never updated on an existing cluster. The API server would prune the
field and the single Sentinel would keep a `minAvailable: 1` PDB that
blocks node drains. A 1.6 fix would need a CRD update path first.


### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [x] No downstream repository is affected by this change (walked the
map: the diff changes a template, a test, and the wording of two
descriptions; no field, default, enum or name changes)
- [ ] [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

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(redis): an instance with `replicas: 1` now runs one Sentinel instead of three, without a Sentinel PodDisruptionBudget; on upgrade, the PDB the operator created earlier is deleted. Instances with two replicas or more are unchanged.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Redis deployments configured with one replica now use one Sentinel;
other replica settings continue to use three.
* Workload monitoring now reflects the configured Sentinel count. When
changing to one replica, upgrades remove the Sentinel
PodDisruptionBudget.
* **Documentation**
* Updated Redis configuration guidance to explain how Sentinel count
varies with the replica setting and clarify Sentinel resource defaults.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:17:32 +03:00
Aleksei Sviridkin d61db784a6 fix(mariadb-operator): derive the cluster name from the platform domain (#5123)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

Fixes #2348.

`packages/system/mariadb-operator/values.yaml` pins
`mariadb-operator.clusterName: cozy.local`, and the vendored chart turns
it into the operator's `CLUSTER_NAME` and into a SAN of the webhook
certificate. On a platform installed with any other
`networking.clusterDomain`, the operator resolves its own pods as
`<name>.<ns>.svc.cozy.local` and gets NXDOMAIN. Observed on a cluster
with `clusterDomain: lab.local`: a 2-replica MariaDB never gets
replication configured (`status.replication` stays `{}`, the secondary
runs as a standalone semi-sync master), and User, Database and Grant
fail with `lookup mariadb-<name>-primary.<ns>.svc.cozy.local ... no such
host`, while the operator only retries at verbosity 1.

`clusterName` is a value of the vendored subchart, which the `_cluster`
lookup used by package templates cannot reach. The platform now carries
`networking.clusterDomain` under the `mariadb-operator` subchart key in
the shared `cozystack-values` Secret, the same route
`keda.clusterDomain` and `opensearch-operator.manager.dnsBase` already
take. The literal in the package stays as the fallback for a render
without that Secret, so a cluster on `cozy.local` renders exactly as
before.

Tests cover both ends of the path:
`packages/core/platform/tests/apps_mariadb_operator_cluster_name_test.yaml`
pins that the Secret carries the domain under the anchored subchart key
and tracks a non-default value (it fails with the `apps.yaml` change
reverted), and
`packages/system/mariadb-operator/tests/cluster_name_test.yaml` pins
that a domain set on that key reaches the operator's `CLUSTER_NAME` and
the webhook certificate, with `cozy.local` kept as the standalone
default. `make helm-unit-tests` passes locally.


### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [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

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(mariadb-operator): the MariaDB operator now uses the platform's networking.clusterDomain instead of a hard-coded cozy.local, so replication and User/Database/Grant reconciliation work on clusters with a non-default cluster domain.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* MariaDB now uses the configured networking domain as its cluster name
and webhook certificate address. If no custom domain is configured, it
continues to use the default cluster name and address.
* **Tests**
* Added coverage for default and custom domain settings, including their
effect on the MariaDB cluster name and webhook certificate address.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:07:37 +03:00
Aleksei Sviridkin 0c486263f9 fix(postgres): render the declared databases in a stable order (#5091)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

The postgres init script lists the declared databases with `keys
.Values.databases`, which returns them in Go's randomized map order. A
release with two or more databases therefore renders a different
`DEFINED_DBS` line on each upgrade, so the `<release>-init-script`
Secret and the `checksum/config` annotation of the init Job change on
upgrades that did not touch the databases. Sorting the keys makes the
render a function of the values alone.

Found while measuring a platform-wide values change on a production
cluster (#5092): of 36 releases that ran Jobs again, 31 rendered a
byte-identical manifest across the two Helm revisions and the other 5
were Postgres releases whose only difference was this ordering, e.g.
`DEFINED_DBS="db_a db_b"` → `DEFINED_DBS="db_b db_a"`.

This does not stop the init Job from running on every upgrade (it is a
`post-upgrade` hook); the issue covers that part.

Verification: the new `tests/init_script_deterministic_test.yaml`
renders eight databases declared out of order and expects them sorted.
Without the fix it failed 5 runs out of 5 with a different order each
time; with it, `helm unittest packages/apps/postgres` passes (27 suites,
210 tests).


### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [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

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(postgres): the init script renders the declared databases in a stable order, so an upgrade that leaves the databases unchanged no longer rewrites the init-script Secret.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Database initialization now lists configured databases in alphabetical
order, providing consistent output across deployments.
* **Tests**
  * Added coverage to verify the alphabetical ordering.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:06:40 +03:00
Aleksei Sviridkin 72ff22dc9e fix(securitygroups): project a membership-only group to a policy Cilium accepts (#5133)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

Fixes #5129.

A SecurityGroup with `attachments` and no rules still projects to a
backing CiliumNetworkPolicy, since that policy carries the attachments
the securitygroup-controller reconciles. Its spec was `endpointSelector`
plus `ingress: []`. The CRD `anyOf` accepts that, but the agent's
`api.Rule.Sanitize` counts entries (`len(r.Ingress) == 0 && ...`, the
same in v1.19.5 and v1.20.2), so Cilium marks the policy `Valid=False`
with "rule must have at least one of Ingress, IngressDeny, Egress,
EgressDeny". `pkg/apis/sdn/DESIGN.md` §5 claimed the opposite.

The projection now writes one empty ingress rule, which allows nothing,
plus `enableDefaultDeny: {ingress: false, egress: false}`, so that rule
does not put the member pods into default deny. The override relies on
the agent's `enable-non-default-deny-policies`, which defaults to `true`
in the vendored chart; DESIGN.md says so. `ingress` is now `omitempty`
like `egress`, because an empty list no longer serves any purpose. The
reverse projection hides that exact spec, so the group still reads back
with no rules. `enableDefaultDeny` is what tells it apart from a
tenant-authored empty ingress rule, which keeps its default deny and
stays visible.

`TestRulesLessSecurityGroupProjectsValidPolicy` now checks the wire form
against the Sanitize rule and fails on `main` (`ingress:[]`).
`TestEmptyIngressRuleIsNotMembershipOnly` covers the tenant-authored
empty rule. `go test ./pkg/registry/sdn/...` passes.

Checked on a lab running Cozystack v1.6.5 with Cilium 1.19.5
(`enable-non-default-deny-policies: true`), in a throw-away namespace
with no baseline policy. A membership-only group `front` attached to
`web`, and a group `db` on `srv` allowing `fromSG: [front]` on 8080. The
v1.6.5 projection gives `front` as `VALID=False`. With the spec this
branch writes for `front` applied to its CNP, it turns `VALID=True`
("Policy validation succeeded"), and every probe is unchanged: `web ->
srv:8080` OK, `web -> srv:9090` and `other -> srv:8080` refused, `other
-> web:8080` and `web -> other:8080` OK, so the placeholder isolates
nothing. Moving the attachment of `front` from `web` to `other` flips
the `fromSG` peer: `other -> srv:8080` OK, `web -> srv:8080` refused.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

The diff touches `pkg/registry/sdn/securitygroup/` and
`pkg/apis/sdn/DESIGN.md` only: no package, chart, CRD, `hack/` or
convention change. The website mentions SecurityGroups only in the v1.6
blog post and the PCI-DSS page, neither of which describes the backing
policy.

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [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

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(securitygroups): a SecurityGroup with attachments and no rules no longer leaves an invalid CiliumNetworkPolicy behind; its backing policy is now accepted by Cilium and still allows or denies nothing.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Membership-only SecurityGroups now round-trip without displaying
ingress or egress rules, while remaining usable without triggering
default-deny behavior.
* SecurityGroups with an explicitly configured empty ingress rule
continue to retain that rule when read back.
* This behavior depends on Cilium’s `enable-non-default-deny-policies`
option being enabled; when disabled, Cilium applies default deny to
every policy.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:05:32 +03:00
Aleksei Sviridkin 9e8e10c307 fix(backups): re-render the default backup objects when the S3 endpoint changes (#5230)
## What this PR does

Refs #5205.

The S3 endpoint the platform backups use is copied twice, and neither
copy is refreshed after `publishing.host` changes. The COSI sidecar
writes `BucketInfo` once per `BucketAccess`, so the credentials Secret
`bucket-cozy-backups-system-credentials` rendered from it keeps the old
endpoint. The Velero BSL and the default Strategy CRs keep whatever
endpoint the last render of `backupstrategy-controller` saw. The default
objects gate only acted on missing objects, so backups kept going to the
old host and nothing reported it.

The gate now also forces a render when a copy is stale:

- the bucket's `<bucket>-system` release, when the credentials Secret's
endpoint differs from the COSI Secret it is rendered from;
- `backupstrategy-controller`, when the endpoint an object was rendered
with differs from the credentials Secret's. The chart stamps the
rendered endpoint on the BSL and on every default Strategy as
`backups.cozystack.io/endpoint`, because the endpoint itself sits at a
different path in every kind. Objects without the annotation count as
stale until a render stamps it; `MinForceInterval` bounds the forces.

Stale credentials are forced before the BackupClass and the routed
objects are read, so an absent BackupClass or an unreadable Strategy
still fails the check but does not keep the old endpoint in the
credentials Secret. A `BucketInfo` without an endpoint is not compared,
since rendering from it would write an empty endpoint or fail the bucket
release. Repairing missing objects keeps working while the credentials
comparison is stale or fails, and a `systemSecretName` that is not
derived from a COSI Secret (no `-credentials` suffix, or the raw COSI
Secret itself) is not compared at all. RBAC is unchanged: the controller
already holds cluster-wide `get` on Secrets. No new metric: nothing
would consume it.

What this does not do: the COSI Secret still holds the endpoint the
`BucketAccess` was granted with, so after a host change the endpoint
follows only once an operator deletes the `BucketAccess` and forces
`bucket-cozy-backups` to recreate it (this issues new S3 keys). From
there the gate replaces the remaining manual steps of the recovery
described in the issue. Automatic re-grant is left for a separate issue,
and the operator runbook goes to the website, hence `Refs` rather than
`Fixes`.

Known limitation: the credentials comparison runs only when
`backupStorage.systemSecretName` is
`bucket-<bucketName>-system-credentials`, the Secret the forced
`bucket-<bucketName>-system` release renders. If an operator sets
`bucketName` and `systemSecretName` inconsistently, the comparison is
switched off rather than forcing a release that never rewrites the
compared Secret, so a stale endpoint in that Secret is not repaired.
Repairing a missing credentials Secret still forces the release derived
from `bucketName`, as before this PR.

Tested with unit tests for the gate (including the comparison
normalisation shared with the chart helpers), helm-unittest for the
annotation and the wiring, and a bats guard that every rendered Strategy
CR and the BSL carry the annotation. On a live cluster, a server-side
render of the chart produced exactly the annotation the gate derives
from the real credentials Secret, so the steady state does not force
anything.

### Screenshots

Not a UI change.

### Downstream repositories

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up: an operator runbook for changing `publishing.host` with
platform backups enabled (re-grant the `BucketAccess`, then let the gate
converge; restart ClickHouse pods that read the credentials from
environment variables). Not filed yet, pending approval of the text.
- [ ]
[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): the backup controller now re-renders the Velero BSL, the default backup strategies and the platform bucket's credentials Secret when their S3 endpoint is stale, so after the platform bucket's BucketAccess is re-granted for a new publishing.host, backups move to the new endpoint without forcing releases by hand.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Backup strategies and storage locations now record their storage
endpoint, enabling detection and repair of outdated endpoint settings
after bucket access changes.
* Bucket credentials and backup objects are refreshed independently when
their endpoints are missing or out of date.
* **Documentation**
* Clarified when endpoint changes trigger updates and how suspended
releases are handled.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:02:42 +03:00
myasnikovdaniil 7097cc5cda feat(release): make backports automatic with backport-batch (#5106)
## What this PR does

Backports become automatic. Fix merged to `main` with `fix` title, or
`security` for fix that comes through security advisory, reaches release
lines through batch that runs on Monday and Thursday, newest line first,
nobody requests, labels or opens backports by hand. Batch runs for two
newest lines with stable release (`released-lines` in policy block) and
every line branched after them that has no stable release yet: with 1.6
and 1.7 released, `release-1.8` created at `v1.8.0-rc.1` joins them, and
`v1.8.0` drops 1.6. Line counts as released once `vX.Y.Z` tag without
pre-release suffix exists on origin. Rules are in
`docs/release/backports.md`, `cmd/backport-batch` applies them,
`.github/workflows/backport-batch.yaml` runs it. Design is in
cozystack/community#114, merge shape of each original in
cozystack/community#119.

- `backport-batch run --line release-X.Y` takes PRs merged to `main`
since line branched (title and body read from merge commit) and skips
what rules exclude: other types and `deps`, breaking changes, platform
migrations, `targetVersion`, changed CRD documents, API changes that API
review gate calls sizeable (batch runs `internal/apigate` itself
whenever PR changes a file the gate parses:
`pkg/apiserver/apiserver.go`, an ApplicationDefinition, or YAML under
`packages/` or `internal/` naming `CustomResourceDefinition` or
`APIService`), and on older line what next newer line doesn't carry yet.
The rest goes onto `backport-batch/<line>-<date>` in merge order. PR
merged to `main` with merge commit goes as its own commits picked with
`cherry-pick -x --signoff`, so each keeps its author, message and
sign-offs and gets sign-off of whoever runs the batch (with `-m 1`
author was whoever merged on `main`), merges of `main` into PR branch
are left out, and merge commit joins them to the batch with original's
merge message and `Backported-from:` trailer. Squash-merged PR goes as
one pick. Conflict on any commit takes branch back to where it was
before that original, it's listed with the commit that failed, run goes
on. `--push` opens or updates draft PR, `--continue` carries on open
batch PR as fast-forward only, PR body it didn't write is never
rewritten. Same-day run over batch branch whose PR never got opened
refuses and names `run --line <line> --onto <head> --push`, which
continues it. Run or `--defer` that resets local batch branch over
commits only it has prints `git branch` command that restores them.
- `run --defer <N> --reason <why>` takes original out of open batch
(usually the one failing CI is pinned on). It rebuilds branch replaying
batch unit by unit without it, so merges and pushed conflict resolutions
keep their shape, and only this rebuild pushes with
`--force-with-lease`. Deferred original comes back in next batch.
- Line carries something only if its own history shows it: picks whose
`-x` line, the one git appends, names PR's merge commit or every commit
of its own, or pick that changes nothing. Picked commits keep their
messages and batch merge carries PR body, so text in them (`-x` lines,
`Backported-from:`, `Backport of #N`) counts only on commits from before
`batch-only` day in policy block, where it records old hand backports
and batches. Otherwise text in a merged fix could mark another fix as
already backported, and it would never reach the line. PR against line
never counts. Open batch PR counts only from this repo and from author
with write access or the batch's own bot (`BACKPORT_BATCH_BOT`, exact
login, `cozystack-ci[bot]` when unset, so maintainer's run by hand
continues scheduled batch instead of opening second one), and is read
the same way. Actions bot isn't trusted anymore, any workflow token
writes as it.
- Any failed GitHub API or git call stops the run with message naming
the call, no retries. Failed permission lookup used to count batch PR
out as look-alike, so `--continue` opened second batch PR and `notify`
posted second comment; now `run`, `check`, `verify` and `notify` exit 2
and write nothing.
- Policy applies fully from `release-1.7`. `release-1.6` takes only
fixes merged from 2026-10-26, older ones need `backport/include`. Start
days are in `backport-policy` block on the policy page, test keeps page
and code in sync.
- `backport-batch check --cut` replaces `backport-audit` as gate before
patch cut and before every rc after the first, it also fails while a fix
is only on open batch PR. Plain `check`, which scheduled run uses, lets
those through. `backport-batch fold` checks conflict resolution in the
same shape: merge on batch head whose second parent is straight line of
picks of PR's own commits in order, each with its `-x`, only PR's files
(one pick for squash). `backport-batch verify` holds every original on
batch to that shape, clean picks included, and `pr-labeler.yaml` runs it
on every push to batch PR: `pull_request_target` so it reaches
`release-*`, tool built from `main`, batch head fetched as data,
read-only token. Batch PR body says per original how many commits it
brought and which picks differ from their commits. `backport-batch
notify` reports batch that is still open from previous run and deferred
conflicts in one comment on batch PR, edited in place, with
`BACKPORT_ALERT_TEAM` mention.
- `hack/backport-batch-scheduled.sh` is what the workflow runs and
maintainer can run by hand (`--dry-run` changes nothing on GitHub): for
each line newest first `run --continue --push`, `check`, `notify`. It
pushes with `BACKPORT_BATCH_TOKEN` only and fails without it, never
`GITHUB_TOKEN`, because its pushes don't start CI. Workflow runs it as
cozystack-ci App: it mints token for this repo only with Contents and
Pull requests write, passes it as `BACKPORT_BATCH_TOKEN`, commits as
`cozystack-ci[bot]`, and sets `BACKPORT_BATCH_BOT` so the tool trusts
that bot's batch PR and comment. Job timeout is 30 minutes, under the
token's hour.
- App has no workflows permission, so workflow runs the script with
`--no-workflow-files`: original that changes `.github/workflows/` isn't
picked and waits on batch PR for maintainer, listed with its workflow
files in body and in notify comment. It's never a conflict. `check`
warns about it while it waits and `check --cut` fails on it. Maintainer
brings it over with own token by `run --continue --push` without the
flag, or takes it out with `--defer`.
- Deferred conflicts and failed batch CI wait for maintainer. `notify`
keeps one comment on batch PR listing stalled batch, conflicts and
workflow changes, mentioning `BACKPORT_ALERT_TEAM` (team or login) when
it's set. Failed batch CI isn't in the comment, it shows on batch PR
checks, and `check --cut` fails while batch is unmerged.
- Two steps that need judgement (resolve conflict, pin failed CI on
original and defer it) are written as runbooks in
`docs/release/backport-tasks/`: inputs, the only allowed output, check
it must pass before push, what never to do.
- `backport/include` and `backport/skip` let maintainer override rules
for single PR, they are new `backport/` category in `labels.yml`. Skip
wins over include, if open batch already picked it `--defer` takes it
out.
- Text written by other people (titles, release notes, reasons, file
names) goes to batch PR body only inside code spans or release-note
block, control, bidi and zero-width characters are dropped, file names
in deferred block included, git args go after `--end-of-options`, no
shell.
- Removed: `cmd/backport-audit`, `backport.yaml`, `kind/backport` and
`kind/backport-previous` labels with aliases and stale exemption,
trailer-check exemption for bot branches. `Commit trailers` job exempts
only `backport-batch/*` branches of this repo, changelog instructions
read originals from batch commits. Exemption lives in main's
`pre-commit.yml`, and batch PR runs its line's copy: `release-1.7` has
its own copy that exempts only old `backport-<pr>-to-<branch>` branches,
`release-1.6` has no such job. So on `release-1.7` picked commit with
bad trailer fails batch CI until line gets main's copy.
- `hack/close-manual-backport.sh` runs from `pr-labeler.yaml` on
`opened` and closes PR against `release-*` from outside contributor
(association contributor, first-time contributor or none, permission
read or none). Owners, members, collaborators, anyone with triage or
more and bots are left alone. When GitHub's answer on author's
permission can't be read, it closes nothing and fails the job naming the
failed read. Maintainer keeps release-only fix by reopening it.
Release-only PRs are occasional and opened by maintainers, for fix with
no counterpart on `main`, or one that on `main` came inside another PR
or was written for `main`; if its counterpart on `main` is a fix the
batch would take, the batch skips it once picking it changes nothing on
the line, otherwise maintainer labels it `backport/skip`.
- `docs/release.md` is split into pages under `docs/release/`, PR
template doesn't ask for `kind/backport` anymore. `security` type is
added to commit format in `docs/agents/contributing.md`, PR template,
Gemini review guide and labeler's list (labeler maps no `kind/*` to it,
`labels.yml` has no fitting one).
- `SECURITY.md`, `docs/security/incident-response.md` and
`GENERAL_TECHNICAL_REVIEW.md` now say two latest minor releases are
supported, same window the batch runs for, and link the backport policy
for which fixes go there.

Dry runs without push, on 2026-11-06, with `--no-workflow-files` as the
workflow runs it: `lines` gives `release-1.7` and `release-1.6`. On
`release-1.7` batch picks 12 fixes, defers 3 conflicts (#4756, #4867,
#5082), leaves #4888 and #4971 to a maintainer because they change
workflows, and skips 49. On `release-1.6` it picks #5088, defers 3
conflicts (#5071, #5086, #5087), leaves #5142 to a maintainer, and skips
590. `verify` passes both batches.

Setup after merge: no new secret. Batch runs as cozystack-ci App from
existing `COZYSTACK_CI_APP_ID` and `COZYSTACK_CI_PRIVATE_KEY`, job fails
when either is missing. App's installation already has Contents and Pull
requests write on all org repos, and no Workflows is needed: PRs that
change `.github/workflows/` wait for maintainer, who brings them over
with own token as "Changes to workflows" in `docs/release/backports.md`
says. Today that's #4888 and #4971 on `release-1.7` and #5142 on
`release-1.6`. Set `BACKPORT_ALERT_TEAM` to a team or a login (for
example `cozystack/maintainers`): unset, batch comment mentions nobody
and nobody is notified. Once this PR is merged, `Backport batch shape`
can be required check in the release lines ruleset (`release-*`
excluding `release-*.*.*`), with "Do not require status checks on
creation" on, because `cut-prerelease.yaml` creates `release-X.Y` by
push. `PR Auto-Label` with both new jobs runs on `pull_request_target`,
so it needs #5135 sorted before 2026-12-02. `kind/backport*` labels have
to be deleted by hand, label sync doesn't delete.

### Screenshots

Not a UI change.

### Downstream repositories

cozystack/community `contributors/README.md` lists commit types, so it
needs `security` too, it has no backport label text. Website has no
backport label instructions, and its links to `docs/release.md` carry no
anchor, the page stays as index. Trigger map lists commit convention
change for cozystack/ccp: this adds `security` type, but not message
format or trailer, and `package-bump` writes only `chore(...)` commits
and restates the trailer, so nothing changes there.

- [ ] 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: add `security` to commit types in `contributors/README.md`

### Release note

```release-note
feat(release): backports are automatic. Fixes merged to main with fix or security title reach release lines through a batch built by cmd/backport-batch every Monday and Thursday, by the rules in docs/release/backports.md. The batch runs for the two newest lines with a stable release and for a line in its rc window: the release-X.Y branch the first rc creates joins at once, and its vX.Y.0 drops the oldest line. Features, chores, docs, dependency bumps, breaking changes, migrations, CRDs and API changes that the API review gate calls sizeable wait for the next minor. The policy applies fully from release-1.7, release-1.6 takes only fixes merged from 2026-10-26. Each original reaches the line as its own commits under a merge commit, keeping their authors and sign-offs. Maintainers override the rules for single PR with backport/include and backport/skip labels and take an original out of a batch with backport-batch run --defer. backport-batch check replaces backport-audit as the gate before a patch release and before a later rc. The label-driven backport workflow and kind/backport labels are removed. Backport PR opened by hand against a release branch by author with read access or less is closed automatically. The scheduled batch runs as the cozystack-ci App and needs no secret of its own; a fix that changes .github/workflows waits on the batch pull request for a maintainer, since the App may not push workflow changes. Release documentation now lives in one page per topic under docs/release/.
```
2026-11-09 09:42:38 +05:00
Mattia Eleuteri 9fba8af69c test(redis): pin the release name in the Sentinel PDB hook for the longest release
The long-name case checked only that the Job name fits in 63 characters and that the Job targets the right PDB. A Job name that dropped the release, letting two releases' hooks replace each other, or a Role granting a different PDB would both have passed.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-11-08 19:45:18 +01:00
Marian Koreniuk e92bc8664b fix(tests): keep two unit tests green after the release image restamp
The release build rewrites packages/*/values.yaml: the kube-ovn image tag
becomes the Cozystack version and the traces collector becomes the
first-party otelcol-tracing build. Two unit tests read exactly those
fields, so every PR against release-1.7 has failed Unit & controller
tests since the first rc, and release-1.8 would repeat it.

Same change as #5179 on release-1.7.

Assisted-by: LLM
Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
2026-11-08 19:37:03 +01:00
Mattia Eleuteri a7f84242d1 fix(redis): keep three Sentinels for a zero or negative replica count
The operator runs three Redis pods when the count is zero or negative, but the chart treated any count up to 1 as the single-pod case. Such a cluster got one Sentinel and no Sentinel PDB, while three Redis pods ran under it.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-11-08 19:36:44 +01:00
Mattia Eleuteri fd9245f008 test(lineage): reject CAPK labels on objects that are not KubeVirt VMs
The bridge from a VirtualMachine to its KubevirtMachine is guarded by
the object's API version and kind, but no test exercised that guard:
weakening it to match on either field alone still passed, and let a
labelled kubevirt.io Cluster or a VirtualMachine of another group
resolve to the Kubernetes application.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-11-08 19:34:49 +01:00
Alexey Artamonov c6c107fd08 docs(backups): state when the gate compares endpoints in the values reference
The values reference read as if the credentials endpoint were always
compared and an object without the endpoint annotation never forced a
render. Operators sizing the force counter or choosing a custom
systemSecretName need both conditions spelled out.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov ee5e51ee6c fix(backups): keep forcing objects that still lack the endpoint annotation
An object without backups.cozystack.io/endpoint stopped counting as
stale as soon as one forced render had been requested, whether or not
that render succeeded. A failed upgrade left the objects unannotated
and the gate silent until the controller restarted, which also hid the
climbing force counter meant to report exactly that failure. Such an
object now stays stale until a render stamps it; MinForceInterval
already bounds the cost.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov dc909c778c fix(backups): compare the credentials endpoint only for the forced bucket's Secret
The gate forces bucket-<bucketName>-system, which renders the
credentials Secret of that bucket's system user and nothing else. The
COSI Secret name was derived from systemSecretName instead, so a value
naming another bucket's Secret made a lagging endpoint there force the
platform bucket release every throttle window without ever refreshing
the Secret that was compared.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov bd2b6c4b49 fix(backups): leave the credentials Secret alone when BucketInfo has no endpoint
A BucketInfo without an endpoint differs from every credentials Secret
that has one, so the gate forced the bucket release on every throttle
window. That render cannot converge: an empty endpoint would be written
into the Secret every driver reads, and with the key absent the
template's trimPrefix fails and parks the release in Failed.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov 399ccd3f4a fix(backups): refresh stale credentials even when the routed objects are unreadable
The credentials endpoint comparison needs only the credentials Secret and
the COSI Secret it is rendered from, yet its forced render was issued only
after the BackupClass and every routed object had been read. A missing
BackupClass or one unreadable Strategy therefore kept the old endpoint in
the credentials Secret, which the projector hands to every driver, not
just the one whose object failed.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov ef7626a951 fix(backups): keep repairing missing default objects while credentials lag
The credentials comparison returned early when the credentials Secret
was stale or the COSI Secret could not be read, so a missing default
Strategy or BSL was no longer created on that tick, nor on any later
one while the bucket release stayed suspended, kept failing, or the
COSI Secret stayed unreadable. Repairing missing objects is what the
gate did before the comparison existed, and it does not depend on the
answer.

Run the object check on every tick and force each release for what it
renders, reporting a comparison error alongside instead of in place of
the repair.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov d0f47f7dd5 fix(backups): compare the credentials endpoint only for a Secret derived from COSI
backupStorage.systemSecretName may name a Secret the bucket chart does
not render from a COSI Secret: the raw COSI Secret itself, which the
projector accepts, or any Secret without the -credentials suffix. The
COSI Secret name was then the source name unchanged, so the gate
compared a Secret with itself and either reported it stale on every
tick or failed the check, and stopped repairing missing default objects
as well. Skip the comparison when there is no distinct COSI Secret or
no endpoint key to compare.

The gate compares the annotation with what the chart's endpoint
helper renders, and the helper renders a present but blank endpoint as
a bare https:// instead of falling back, so the gate has to compute
the same value or its expectation and the chart disagree for that
Secret. The gate's docs, metric help and log lines named only missing
objects, so they did not cover a stale endpoint they now report.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Kirill Ilin cb3fcd3357 feat(operator): let the platform source trust a private registry CA (#5237)
## What this PR does

The operator creates the `cozystack-platform` OCIRepository from its own
command-line arguments and rewrites the whole object on every start;
`cozystack-packages` copies that spec. When the platform source is a
registry whose certificate is issued by a private CA (an internal
pull-through cache in an air-gapped install, for example), there is no
durable way to make Flux trust it: a `certSecretRef` patched onto the
live object is dropped on the next operator restart, source-controller
then fails TLS, and platform updates silently stop arriving.

This adds `--platform-source-cert-secret` to the operator and
`cozystackOperator.platformSourceCertSecret` to the installer chart. The
named Secret in `cozy-system` is set as `spec.certSecretRef` on the
generated OCIRepository, so it holds `ca.crt` for a private CA and/or
`tls.crt`+`tls.key` for client certificate auth, with the Flux key
layout. `cozystack-packages` inherits it through the existing spec copy.

The flag is OCI only. `GitRepository` has no `certSecretRef` (Flux reads
a Git CA from the `ca.crt` key of the `secretRef` Secret), so a Git
platform source with the flag set is refused at startup instead of
having its CA silently ignored.

Tests: unit tests for the generated spec, a regression test that the CA
survives the operator's update of an existing OCIRepository, a test that
a Git source with the flag writes nothing, and a helm-unittest suite for
the new chart value.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [ ] No downstream repository is affected by this change
- [x] [cozystack/website](https://github.com/cozystack/website) -
follow-up: https://github.com/cozystack/website/pull/791
- [ ]
[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:

The new installer value is optional, so `ansible-cozystack` is not
reached. The website's air-gapped guide
(`content/en/docs/next/install/kubernetes/air-gapped.md`) added the
private CA to the nodes only, while Flux fetches the platform source
itself; cozystack/website#791 adds a step for `platformSourceUrl`,
`platformSourceSecret` and `platformSourceCertSecret`.

### Release note

```release-note
feat(operator): the platform source can be served from a registry behind a private CA. Set `cozystackOperator.platformSourceCertSecret` in the installer to a Secret in `cozy-system` with `ca.crt` (and optionally `tls.crt`/`tls.key`); the operator sets it as `certSecretRef` on the `cozystack-platform` OCIRepository, so it survives operator restarts.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Added the `platformSourceCertSecret` setting for OCI platform sources.
It allows a Secret containing certificates for private CA trust or
client authentication to be configured separately from registry
credentials.
* The configured certificate Secret is applied to the OCI source and
inherited by its package source.
* **Configuration**
* The setting is empty by default. Git platform sources do not support a
separate certificate Secret; use the existing platform source Secret for
Git TLS certificates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-06 22:21:19 +05:00
Myasnikov Daniil 41507e36aa docs(kubernetes): say where worker registry mirrors are configured
The air-gapped note said in-guest image pulls honour no per-tenant
mirror until a Phase 2 that has landed, so an operator setting up an
air-gapped cluster would read that mirroring is unavailable and file
an issue. It now says the Secret and the platform registries value are
removed, that a Package still setting the value keeps installing, and
that worker mirrors are set per pool through talos.registryMirrors of
the KubernetesNodes resource.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 18:56:09 +05:00
Kirill Ilin 0a448963e3 docs(installer): say a CA-only cert secret must be Opaque
kubernetes.io/tls requires both tls.crt and tls.key, so a Secret that
carries only ca.crt is rejected under that type.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-06 18:29:18 +05:00
Kirill Ilin fbfc270c71 test(installer): tie each platform source flag to its own value
Both secrets were only ever set together, so a cert flag keyed on
platformSourceSecret stayed green. A pull-through cache behind a private
CA usually needs the CA without credentials, which is exactly the case
such a slip breaks. The absence checks match the flag by prefix, since
a whole-element comparison misses the flag rendered with any value.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-06 18:29:18 +05:00
Kirill Ilin dfd7cbed36 fix(operator): reject a Git cert secret before installing CRDs and Flux
CRDs and Flux are installed before the platform source, so refusing a
cert secret on a Git source only when the source is written exits the
operator after a partial install. The check now runs right after flag
parsing, and the source install reuses it.

The guard runs on every start, so a Git source without a cert secret is
now covered over https and ssh: a guard that misfired there would
crash-loop the operator for every Git platform source. The update test
also renames the cert Secret, as a CA rotation does.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-06 18:28:45 +05:00
Myasnikov Daniil 1ceaeb8b6e docs(security): support the two latest minor releases
The security policy promised three supported lines and said no rule
decides which of them a fix reaches. The backport batch runs for the
two newest lines with a stable release, by the rules on the backport
policy page, and the maintainers agreed to make that the support
window. SECURITY.md, the incident-response summary and the technical
review now name the two latest minor releases, say that the older one
drops out when a new minor ships, and send a reader to the backport
policy for which fixes reach them.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:37 +00:00
Myasnikov Daniil eaa0169ff3 chore(release): retire the backport bot, its labels and backport-audit
The batch replaces the label-driven backport workflow and the audit
that gated a cut against it. The kind/backport labels, the stale
exemption for them, their contract tests and the audit's make target
go with them. labels.yml gains a backport/ category instead, with
backport/include and backport/skip, the overrides the batch reads off
a merged pull request.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:37 +00:00
Myasnikov Daniil 4a750a1e23 ci(release): run the backport batch twice a week
The batch runs on Monday and Thursday from a script a maintainer can
also run by hand. It runs as the cozystack-ci App with a token for this
repository alone and Contents and Pull requests write, so its pushes
start CI on the batch and no person's token is involved. The App may
not push a workflow change, so such an original waits for a
maintainer.

pr-labeler holds every batch pull request to the batch's shape, with
the tool built from main and the head read as data. It also closes a
pull request into a release line opened by someone with read access
or none, since fixes reach the lines through the batch. The Commit
trailers check exempts a batch branch of this repository, whose picked
messages cannot be rewritten. Promotion now says how to bring its own
pipeline to a line through the batch.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:37 +00:00
Myasnikov Daniil 9f8c88b4f1 feat(release): build the backport batch with backport-batch
The rules need a tool that applies them the same way on every run and
shows what it did. backport-batch reads main's first-parent history,
judges each pull request by the policy block it reads from the page on
main, and picks what qualifies onto one batch branch per line. Each
original lands as its own commits under a merge that names it, so
authors and sign-offs survive and the line's own history shows what it
carries.

check is the gate before a cut. fold holds a hand resolution of a
conflict to the batch's shape, verify holds a whole batch to it, and
notify reports what waits on the batch for a maintainer.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:36 +00:00
Myasnikov Daniil c12dcdddb4 docs(release): make backports automatic, by written rules
Backports were asked for pull request by pull request with labels and
opened by a bot, so whether a fix reached a release line depended on
someone remembering to ask, and the lines drifted apart. The policy
page states the rules instead: a fix or security fix merged into main
goes to every line the batch runs for, unless it is breaking, touches
platform migrations or a CRD, or changes the API in a way the API
review gate calls sizeable. Two labels let a maintainer override the
rules for one pull request.

The task pages give a maintainer the steps for a deferred conflict and
for a failed batch CI. The backports route in AGENTS.md, the
contributing and changelog guides and the PR template point at the
policy, and the commit format lists the security type the policy
takes.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:35 +00:00
myasnikovdaniil 970191dc8a fix(clickhouse): restore the archive a Backup was taken as (#5208)
## What this PR does

A ClickHouse `RestoreJob` restored the newest archive under the app
prefix, whichever `Backup` it named. Strategy pod generated the archive
name with `date -u` and nothing was recorded on the `Backup`, so restore
could only list the remote archives and take the last one. Restoring an
older `Backup` brought back the latest data.

Now the controller names the archive before it creates the backup Job
(`clickhouse-<app>-<startedAt>` from persisted `status.startedAt`, so
retried pods write the same one), passes it to the pod as `.ArchiveName`
and records it in the `Backup` `driverMetadata`. Restore takes exactly
that archive and fails with its name when storage has no such archive.
The name is recorded only when the Job's pod got it, so a Job left from
the previous controller during an upgrade, or a custom strategy with its
own names, leaves no name instead of a wrong one.

A retried pod reuses the name, so it waits for an attempt the sidecar
still runs (no 423) and asks for `resume`.

A `Backup` outlives its `BackupJob` and a Plan names `BackupJob`s after
the schedule slot, so a `BackupJob` created again under a used name met
the earlier run's `Backup` when its Job completed, took it as its result
and left the archive it just wrote recorded nowhere. Now it fails if
that `Backup` records another archive, and the message names both. A
retry of the same run still gets its own `Backup` back.

Old `Backup`s have no recorded name and stay restorable: the pod logs a
`WARNING` and takes the newest archive stamped no later than `takenAt`,
and fails when nothing qualifies. While the chart rolls out, previous
controller pod can still render the new template and pass no `takenAt`,
so a restore of a `Backup` without recorded name fails before it touches
any table, and works when created again after the rollout.

Fixes https://github.com/cozystack/cozystack/issues/5199

Tests are go unit tests for naming, recording, a reused `BackupJob` name
and restore render, and `hack/altinity-backup-strategy.bats`, which runs
the shipped script against a stubbed clickhouse-backup API (the restore
tests go red on the old template). Checked on a live ClickHouse with two
backups of 1500 and 2000 rows: restore of the older one by recorded name
ends with 1500 rows, fallback for an unnamed older backup picks the same
archive with the warning, a missing archive fails without touching the
table, a retry over an existing local backup completes under the same
name. The controller binary itself was not run on the cluster, only its
rendered output in the pod.

### Downstream repositories

- [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(clickhouse): a RestoreJob now restores the archive its Backup was taken as, not the newest one. New Backups record the archive name, Backups taken before this release are restored best effort from the newest archive stamped no later than their takenAt, with a warning in the restore pod log. A ClickHouse BackupJob created again under a name whose Backup still exists now fails instead of reporting that Backup as its result.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Backups now use a consistent archive name tied to the backup job, and
completed backups retain that name for future restores.
* Restores can select the recorded archive and backup timestamp. If no
archive was recorded, the system selects the newest eligible archive for
the application.
* **Bug Fixes**
* A restore now fails when its recorded archive is missing instead of
silently selecting a different one.
* Backup requests avoid starting a duplicate action when the matching
action is already in progress.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-06 11:54:38 +05:00
myasnikovdaniil 18779dddfc fix(backups): report sizeBytes for MariaDB and CNPG backup artifacts (#5223)
## What this PR does

Fixes https://github.com/cozystack/cozystack/issues/4316.

Continues https://github.com/cozystack/cozystack/pull/5015 by androndo
on a separate branch. All ten of their commits are kept as they were
(authorship, messages and order, only rebased on current `main`).
MariaDB and CNPG backups stored in S3 report
`Backup.status.artifact.sizeBytes`, the size of the dump
mariadb-operator uploaded or the total of the barman base backup, and
the field stays unset when the size cannot be attributed to that one
backup.

Four commits are added on top:

- `test(backups)`: operator Backup fixtures carry the condition
mariadb-operator writes on success, `Complete=True` with reason
`JobComplete` and message `Success`. Operator sets `Complete=True` for a
failed Job too (reason `JobFailed`), so a fixture without reason
describes a state that never happens.
https://github.com/cozystack/cozystack/pull/5207 makes the driver check
that reason, the tests pass with and without it.
- `refactor(backups)`: the MariaDB matcher returns the matched dump
(key, file name, timestamp from the name, size, LastModified) and tells
"not found" from "ambiguous", and the Secret lookup and S3 client live
on a small value any reconciler can build from its own client. A MariaDB
restore needs this to restore the dump of the Backup it was given and
not the newest one under the prefix. `sizeBytes` does not change here.
- `fix(backups)`: the probe reads bucket, prefix, endpoint, credentials
and TLS from the operator Backup's own `spec.storage.s3`, not from the
strategy as it renders now, so editing the strategy during a backup does
not send the probe somewhere else.
- `fix(backups)`: a dump is attributed only when its ownership is
proven. Before, a backup pod with a clock more than a minute behind,
right after a previous run, got that run's dump size (666 bytes for a
123-byte dump in a test). Now the dump has to be the only one that
either clock places in the operator Backup's lifetime, both clocks have
to agree, and neither may place it in the lifetime of another finished
operator Backup with the same bucket and prefix. Otherwise the size
stays unset. Two cases are still not covered. First, this run's own dump
is missing from the listing while the operator Backup of a neighbour was
deleted or has not finished, then the neighbour's dump can be taken.
Second, backup pod clock and object store clock are both off the same
way by more than the minute of slack, which moves a finished neighbour's
dump into this run's window, and nothing the clocks record tells them
apart. A run whose Job failed has no dump of its own and reaches the
probe only because the driver takes `Complete=True` for success,
https://github.com/cozystack/cozystack/pull/5207 fixes that.

Of the open threads on the original PR, the cache reads, the dump file
name layout and the default AWS endpoint test were already handled by
its later commits, the clock skew one is the last commit here and the
failed Job case is https://github.com/cozystack/cozystack/pull/5207.

`minio-go` is new to this tree, this PR adds it as a direct dependency
and `internal/backupcontroller/artifact_size.go` is its only importer.
The open Bucket strategy PR
https://github.com/cozystack/cozystack/pull/4235 picks the same client
at the same version.

Each clock skew case, and a run whose own dump is gone while the
neighbour's operator Backup still exists and has finished, have a unit
test that fails on the old matcher, and every new guard has a mutant
that turns a test red. I also ran the matcher against a real bucket
after four MariaDB backups in a row. Runs that finished more than a
minute apart mapped to the dump their operator Job logged, the two back
to back came back unset instead of taking each other's dump, and with a
run's own dump hidden from the listing the matcher refused the
neighbour's dump that the old one returned.

### Screenshots

N/A, no UI changes.

### Downstream repositories

The only API change is the description of the existing
`Backup.status.artifact.sizeBytes` field, no new field, value, default
or enum, no package, values or tooling change, and neither
cozystack/website nor cozystack/terraform-provider-cozystack references
`sizeBytes`.

- [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): MariaDB and CNPG (Postgres) backups stored in S3 now report `status.artifact.sizeBytes`: the size of the dump mariadb-operator uploaded, or the total size of the barman base backup (WAL archive excluded). A MariaDB backup takes the size of the one dump that falls inside its run on both the backup pod's and the object store's clock and inside no other finished run, and the size is left unset rather than set to 0 whenever it cannot be measured or attributed. Velero-based backups intentionally keep it unset.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Backup artifacts now report the stored size attributable to an
individual MariaDB or CNPG backup when it can be determined. Shared or
ambiguous storage may leave the size unset.
* **Bug Fixes**
* Improved handling of backup artifact creation and lookup errors,
including retrying when an existing backup cannot be read and failing
jobs when artifact creation is rejected.
* **Documentation**
* Clarified that an unset size means unknown; zero does not indicate an
unknown size.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-06 11:54:28 +05:00
Kirill Ilin 7f1227e3bd feat(installer): expose platformSourceCertSecret
Installing from a registry behind a private CA needs the operator's new
--platform-source-cert-secret flag; without a chart value it can only be
reached by editing the Deployment, which the next helm upgrade reverts.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-05 20:19:35 +05:00
Kirill Ilin c15a0d01b8 feat(operator): let the platform source trust a private registry CA
The operator generates the cozystack-platform OCIRepository from its own
arguments and rewrites it on every start, and cozystack-packages copies
that spec. A registry behind a private CA therefore had no durable way
to be trusted: a certSecretRef patched onto the live object is dropped
on the next operator restart, after which source-controller fails TLS
and platform updates stop arriving.

--platform-source-cert-secret sets spec.certSecretRef from the operator
itself, so the CA (or a client certificate) survives the rewrite. Git
sources are refused instead of ignored, because GitRepository has no
certSecretRef and Flux reads a Git CA from the source secret instead.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-05 20:19:29 +05:00