Commit Graph
454 Commits
Author SHA1 Message Date
Aleksei Sviridkin 7d44372a16 fix(postgres): read the live Cluster when deciding the WAL-archive prefix (#4299)
<!-- 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 #4298.

`applyClusterPluginBackup` preserves the `serverName` already on the
Cluster so that a backup does not flip the WAL-archive prefix
mid-stream, which would split the archive across two prefixes. That rule
is right; the read it rests on is not reliable in one window.

The value was read through the manager's cached client, and
`BackupJobReconciler.SetupWithManager` registers `For(&BackupJob{})` and
nothing else, so a Cluster only reaches the cache through a lazily
started informer with no resync tied to the Cluster's own changes. An
in-place `RestoreJob` deletes the Cluster and the chart re-renders it
with `bootstrap.newServerName` seconds later, so a read served during
that window describes the object that was purged — and the source's
`serverName` is written back over the freshly rendered one.
`barman-cloud-check-wal-archive` then refuses the destination with
`Expected empty archive` and the `RestoreJob` ends on its 30-minute
deadline, which is the failure `newServerName` was added in #3988 to
prevent.

The same stale read carries a stale `UID`, and that `UID` owns the
`ObjectStore` this function applies, so the store is garbage-collected
as soon as it is written.

Reading through the dynamic client this reconciler already carries keeps
the preservation rule and makes the value it rests on truthful. There is
precedent in the same file: the HelmRelease suspend/resume path reads
that way for the same reason.

Observed on a v1.6.3 cluster, twice on separate runs with the same
four-second gap between the chart's apply and the driver's. The
regression test makes the two clients disagree the way they disagree in
that window and asserts the driver believes the live one; reverting the
fix fails it on the serverName assertion.


### 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): an in-place restore no longer loses its fresh WAL-archive prefix to a stale Cluster read, which left the restored cluster archiving onto the source's prefix and the RestoreJob failing on its deadline
```


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

* **Bug Fixes**
* Backup processing now uses the latest CNPG Cluster state rather than
potentially outdated cached information. This ensures the cluster’s
current server name and identity are used when associating backup
ObjectStore resources, including when the cached and live cluster
details differ.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 21:54:01 +03:00
Aleksei Sviridkin 8ed34166cf perf(lineage): cache owner GETs and short-circuit re-admission (#2667)
## What this PR does

The lineage mutating webhook walks the owner chain (Pod, StatefulSet,
HelmRelease, application CR) on every admission, with one uncached
dynamic GET per level, and sibling Pods of the same parent repeat the
same lookups. On a multi-tenant cluster this put `lineage.cozystack.io`
at 300-500ms p99 in
`apiserver_admission_webhook_admission_duration_seconds`, the largest
share of admission latency.

Owners are now cached in-process for 60 seconds, and only successful
lookups are stored. A failed lookup (RESTMapping, NotFound, timeout)
leaves the walk without an owner, so the object is admitted with no
lineage labels and stays unlabeled until its next UPDATE runs the walk
again. Secrets, Services and PVCs can go a long time between updates, so
a cached failure would leave every object admitted within the TTL
unlabeled for that long. The aggregated cozystack-api makes such
failures routine, since discovery of `apps.cozystack.io` fails while it
rolls. The one staleness left is the scheduling class, which is read
from the application's spec: a change to it reaches newly admitted Pods
up to 60 seconds later.

The cache is not swept below 4096 entries, so up to that many owner
objects stay in memory for the life of the process. Above that, each
insert drops the expired entries among a random sample of 64, which
keeps the write lock short.

Non-Pod objects that already carry every lineage label,
`internal.cozystack.io/tenantresource` included, skip the walk, because
the objectSelector still matches an UPDATE whose old object lacks the
label. Pods go through regardless, as each Pod admission also sets the
scheduling class.

### Screenshots

Not a UI change.

### Downstream repositories

The diff is Go under `pkg/lineage` and
`internal/lineagecontrollerwebhook` only: no label, annotation, CRD,
values key, `hack/` file or telemetry metric is added or renamed, so no
entry of the trigger map applies.

- [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
perf(lineage): the lineage mutating webhook caches owner lookups for 60 seconds and skips the owner walk for non-Pod objects that already carry lineage labels, cutting admission latency on clusters with many tenants.
```


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

## Summary by CodeRabbit

* **New Features**
* Lineage and owner lookups now use a time-limited in-memory cache,
reducing repeated API requests during admission processing.
Scheduling-class updates may take up to 60 seconds to appear on new
Pods.
* Non-Pod objects with complete lineage labels are allowed without
additional owner lookup or mutation.
* **Tests**
* Added coverage for cache hits, misses, expiration, and retrying
lookups after errors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 21:34:19 +03:00
Aleksei Sviridkin 018b49929f feat(monitoring): auto-route tenant logs using namespace labels (#2195)
<!-- 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

A tenant that runs its own monitoring stack gets a VictoriaLogs
instance, but the node fluent-bit in `cozy-monitoring` sends every
container log to `global.target` (`tenant-root`), so that instance stays
empty. This PR routes each namespace's container logs to the
VictoriaLogs named by its `namespace.cozystack.io/monitoring` label, the
label the tenant chart already sets and the per-tenant vmagent already
selects on.

**Chart (`packages/system/monitoring-agents`).**
`templates/_tenant-log-routes.tpl` looks up the namespaces and keeps
those whose label is set and names a tenant other than `global.target`.
For those, fluent-bit gets one `rewrite_tag` filter with a `Rule` per
target (a single emitter however many tenants are routed), one `http`
output per target to `vlinsert-generic.<target>.svc:9481`, and a
per-target `modify Set tenant <target>`, because the re-tagged record
re-enters the filter chain where the `Match *` `Add tenant` cannot
overwrite the key. Unlabelled namespaces, empty labels and labels equal
to `global.target` stay on the default outputs.

**Controller (`internal/controller/tenantlogrouting`).** `lookup`
results are not part of the digest helm-controller compares, so the
chart alone would not pick up a tenant that turns monitoring on, or drop
one that is removed, until an unrelated upgrade. The new reconciler in
cozystack-controller digests the monitoring label across all namespaces
and, when the digest moves, sets `reconcile.fluxcd.io/requestedAt` and
`reconcile.fluxcd.io/forceAt` on `cozy-monitoring/monitoring-agents`,
which forces an upgrade and re-runs the lookup. It digests every
labelled namespace rather than repeating the chart's selection, so it
cannot drift from the chart; the cost is an occasional upgrade that
renders the same config. The digest it acted on is kept in its own
annotation, so a manual `flux reconcile` does not make it force another
upgrade. The existing ClusterRole already grants the verbs it needs.

**Behaviour change for the platform operator.** Routed records are
re-tagged with `keep false`: once a tenant runs its own monitoring, its
namespaces' container logs go to that tenant's VictoriaLogs and no
longer to the root one. Events and audit logs are not routed and stay on
the root store.

**Why not a per-tenant collector.** An earlier comment on this PR
proposed a fluent-bit per tenant, following the per-tenant vmagent and
the OTLP collector from #3763. That collector receives OTLP over the
network; a log collector has to tail `/var/log` through `hostPath`,
which the PodSecurity level of tenant namespaces rejects, and running it
in a privileged system namespace instead would mean one DaemonSet per
tenant tailing every node's logs. The routing therefore stays in the one
node DaemonSet, with the reconciler closing the reconcile gap.

Tests: `tests/fluent-bit_tenant_log_routing_test.yaml` mocks the
namespaces with `kubernetesProvider` and pins which get a rule and an
output, which are skipped (unlabelled, empty label, label equal to
`global.target`), and that a routed record carries the target tenant.
Three of its cases fail against the previous revision of this branch.
`tests/fluent-bit_tenant_log_routing_offline_test.yaml` pins that an
offline render routes nothing.
`internal/controller/tenantlogrouting/reconciler_test.go` covers first
sight, no-op while labels are unchanged (including after a manual
reconcile), a tenant turning monitoring on, a routed namespace being
deleted, and a missing release.

Refs #2194

### 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 entry of the trigger map matches the diff: it touches
`packages/system/monitoring-agents` (values, a template helper, tests),
a new reconciler in `internal/controller`, its registration in
`cmd/cozystack-controller`, and a comment in the cozystack-controller
ClusterRole. The one open question is `cozystack/website`:
`content/en/docs/next/operations/services/monitoring/logs.md` describes
log collection in general terms and does not say where a tenant's
container logs land, so the behaviour change above could deserve a
sentence there. I have left every box empty for a maintainer to decide.

- [ ] 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
feat(monitoring): container logs from a tenant that runs its own monitoring stack now go to that tenant's VictoriaLogs instead of the root one. The node fluent-bit routes them by the namespace.cozystack.io/monitoring label, and cozystack-controller re-renders the node agents when that label changes on any namespace.
```
2026-09-29 21:18:52 +03:00
Mattia Eleuteri ca24d8f5f6 perf(lineage): cache owner GETs and short-circuit re-admission
Every admission walks the owner chain (Pod, StatefulSet, HelmRelease,
application CR) with one uncached GET per level, and sibling Pods
repeat the same lookups. On a cluster with many tenants that put the
lineage webhook at 300-500ms p99, the largest share of apiserver
admission latency.

Owners are now reused for 60 seconds, successful lookups only. A failed
lookup, whether RESTMapping, NotFound or a timeout, leaves the walk
without an owner, so the object is admitted with no lineage labels and
stays unlabeled until its next UPDATE runs the walk again. Secrets,
Services and PVCs can go a long time between updates, so a cached
failure would leave every object admitted within the TTL unlabeled for
that long, and the aggregated cozystack-api makes such failures
routine: discovery of apps.cozystack.io fails while it rolls.

Non-Pod objects that already carry every lineage label, tenantresource
included, skip the walk, since the objectSelector still matches an
UPDATE whose old object lacks the label. Pods go through it regardless,
as each Pod admission also sets the scheduling class.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-09-29 17:32:13 +02:00
Aleksei Sviridkin 9b56e70ca6 feat(mongodb): support the system-bucket backup flow (useSystemBucket) (#3967)
## What this PR does

MongoDB was the only `cozy-default` app with **no `useSystemBucket`
flow**: its backup storage was always static on the
`PerconaServerMongoDB` CR (bucket/endpoint/credentials from `backup.*`
values plus a chart-emitted `<release>-s3-creds` Secret), so a tenant
could not back up to the platform system bucket out of the box the way
Postgres/ClickHouse/MariaDB/etcd do. This closes that gap so the default
MongoDB backup-restore works OOTB.

PBM takes `bucket`/`endpointUrl`/`region`/`prefix` as **static CR
fields** (not secret refs), and the platform bucket name/endpoint are
only known at BackupJob time — so, unlike ClickHouse's runtime
`secretKeyRef` env, the chart cannot render them. The implementation
mirrors the CNPG driver's pattern:

- **Chart** (`packages/apps/mongodb`): a new `backup.useSystemBucket`
flag keeps `spec.backup.enabled` (so the pbm agents run) but omits the
static `storages`/`tasks`/`pitr` and the `<release>-s3-creds` Secret.
Helm never owns `spec.backup.storages`, so a Flux re-render can't fight
the driver. A `bootstrap.enabled + useSystemBucket` render guard mirrors
Postgres, and `useSystemBucket: true` with `backup.enabled: false` is
refused both at render time and by an `x-kubernetes-validations` rule on
`backup` in the schema (the same mechanism as the `storageClass`
immutability rule). Note on the schema rule: the aggregated apiserver
does not evaluate CEL on application-backed resources yet (#2657), so
today it is a UI/schema-level marker and the render-time refusal is what
stops a Flux release; it also trips the API review gate (“validation
rule added”), so this PR needs an API-owner approval on top of the code
review.
- **Strategy** (`cozy-default-mongodb`): now carries the S3 coordinates
(`endpointUrl` already scheme'd by the
`backupstrategy-controller.endpoint` helper, `credentialsSecret:
cozy-backups-creds`, `prefix: <namespace>/<application>`).
- **Driver** (`psmdbstrategy_controller.go`): SSA-injects
`spec.backup.storages[s3-storage]` onto the live `PerconaServerMongoDB`
at BackupJob time under a dedicated field manager
(`cozystack-psmdb-backup-driver`) with `ForceOwnership`. Injection is
**gated on the app's `backup.useSystemBucket` flag** (a legacy app
leaves it false and is never touched; a useSystemBucket app is
re-injected once per mint (once per BackupJob, plus once per
storage-race re-mint), before its operator CR is created, so a later
coordinate change catches up on the next job while a streaming dump is
never patched under). The backup CR is minted only once the operator
reports the storage pushed into pbm (`status.observedGeneration` caught
up and `PBMReady` True): pbm writes to what its own config says, not to
the spec, so a CR minted before that push would land at the previous
coordinates while its Backup recorded the new ones; the job waits at
`PerconaServerMongoDBPBMConfigPending` meanwhile, bounded by the backup
deadline. Once a job has its CR, none of the mint-time reads or holds
can fail it: the strategy and app reads the artifact still needs are
bounded by the 24h running ceiling instead, and the cluster is not read
at all **and on the release having rendered that flag**: the app CR is
projected from the HelmRelease's desired values, so the flag reads true
before helm-controller renders the revision, while the tenant's own
storage, scheduled tasks and PITR stream from the previous render still
sit on the cluster. As long as the cluster carries `spec.backup.tasks`
or `spec.backup.pitr`, the driver does not inject and holds the
BackupJob at `Ready=False PerconaServerMongoDBLegacyRender` (failing it
at the deadline), so a wedged legacy revision never has its nightly dump
and oplog redirected into the shared bucket. The backup snapshot falls
back to the strategy coordinates when the operator status echo is empty;
that fallback is gated on the CR's own `percona.com/delete-backup`
finalizer (the flow the CR was minted on), not on the app flag re-read
at completion, so a legacy backup never records the platform bucket and
a flag flipped mid-dump does not change the artifact. The restore path
rebuilds `backupSource` from it (the `cozy-backups-creds` reference is
projected into the restore namespace before dispatch).
- **Retention/cleanup**: on the `useSystemBucket` flow the driver owns
the shared-bucket archive (the chart's psmdb `tasks[].keep` retention is
gated out, and an on-demand backup is not covered by task retention). It
stamps the operator's `percona.com/delete-backup` finalizer on each
backup it mints **against a storage that carries the platform
coordinates** — the strategy's bucket and `credentialsSecret`, read back
from the live cluster after the injection — never on the flag alone: a
strategy without an `s3` block, or a storage that points anywhere else,
holds the job (`MongoDBStrategyHasNoS3` /
`PerconaServerMongoDBStorageForeign`) instead of minting a finalized
dump against a bucket the platform does not own. On Cozystack `Backup`
deletion (by hand today; by platform retention once it lands, since a
`Plan` only creates BackupJobs and never deletes a `Backup`) the driver
deletes the operator `PerconaServerMongoDBBackup` CR so that finalizer
frees the object — the finalizer's presence is the ownership marker
cleanup reads back. That reading is unambiguous because cleanup resolves
the CR by the name recorded in `driverMetadata` (the one the driver
minted), not by scanning: the psmdb operator does stamp
`percona.com/delete-backup` on its own `cron-*` task-minted backups, but
those are never in scope. A legacy backup keeps the pre-existing
hands-off contract, decided from the snapshot before any read of the
operator CR. A terminating namespace or the
`backups.cozystack.io/skip-artifact-cleanup: "true"` annotation strips
the driver's finalizer and releases; a `Get` of the operator CR that
fails with anything other than NotFound/NoMatchError still requeues, and
the operator's own `release-lock` finalizer can hold the CR while the
operator is down, so those two are what can keep a namespace in
`Terminating`. `schedule` and `retentionPolicy` are inert on this flow
(there is no engine-side retention and a `Plan` has no retention field
yet); platform-level retention is tracked in
[#4192](https://github.com/cozystack/cozystack/pull/4192) and teardown
GC of orphaned backups in
[#3966](https://github.com/cozystack/cozystack/issues/3966), so until
those land bound `cozy-backups` with an S3 lifecycle policy.
- **Bounds on the waits**: the injection race with the operator's
cluster cache (an `unable to get storage` error on a freshly injected
storage) is retried by deleting the errored CR and minting a fresh one,
with the retry count kept on the BackupJob
(`backups.cozystack.io/mongodb-storage-race-retries`) because the
deleted CR is reaped before the next poll; after three retries the
operator's error is terminal. A system-bucket dump that has started
streaming is not cancelled on the 30-minute deadline, but fails after 24
hours in `running` (`BackupAbandoned` warning naming the operator CR,
which is left in place as the handle to whatever pbm still writes). The
deadline cancel of a not-yet-started CR pins its delete to the
resourceVersion the live read answered for.
- **RBAC**: `patch` on `perconaservermongodbs` for the injection
(server-side apply), and `create`/`delete`/`patch` on
`perconaservermongodbbackups` — `delete` to prune the shared-bucket
archive on `Backup` deletion, `patch` to strip the driver's finalizer
when releasing a wedged cleanup.

Coverage: Go unit tests over `internal/backupcontroller` — the injection
gate keyed off the app flag (legacy cluster never patched) and off the
applied render (a cluster still carrying tasks or pitr is held,
untouched), the SSA-injection precondition with the whole entry
(per-application prefix included) asserted on the cluster, strategy
templating per application, the finalizer refused against a storage
without platform coordinates (strategy without `s3` over a legacy entry,
foreign coordinates on the merged entry), the retry budget driven with
the operator simulated reaping the deleted CR between polls, the restore
snapshot fallback and credential swap across all four legacy/system flow
combinations, the finalizer-owned cleanup with its escape hatches (skip
annotation before any read, `NoMatchError` treated as gone,
terminating-namespace release using the pre-read CR, a failed strip
surfaced as an Event, a legacy CR never deleted, a legacy snapshot
released without a read, a system-bucket snapshot reaching the prune),
the deadline cancelling a not-yet-running operator backup only on a
positive live read (including no live client at all, and a CR that moved
since the read), and the running ceiling. Plus helm-unittest on both
charts (chart skips the creds Secret and omits
`spec.backup.storages`/`tasks`/`pitr` on `useSystemBucket` and keeps
them on the legacy flow; the strategy renders the S3 coordinates and the
`insecureSkipTLSVerify` knob). A live `useSystemBucket` backup+restore
e2e against the platform bucket is a follow-up (needs the system bucket
wired in CI, same status as the Postgres/ClickHouse useSystemBucket
e2e). One item to confirm in e2e: the psmdb operator accepting
`spec.backup.enabled=true` with an empty `storages` between install and
the first BackupJob.

### Screenshots

Not applicable (no UI changes).

### Downstream repositories

The tenant-facing schema surface is one new field,
`spec.backup.useSystemBucket`, on the MongoDB `values.schema.json` / CRD
(regenerated), plus a validation rule on `spec.backup`.
`terraform-provider-cozystack` hand-models MongoDB and may want to
expose it, but it is a new optional boolean that defaults false (no
drift for existing users), and the chart `README.md` regenerates on
release. Leaving every box unticked per the trigger-map guidance so a
maintainer decides on the provider follow-up.

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

### Release note

```release-note
feat(mongodb): MongoDB backups now support the platform system bucket via backup.useSystemBucket, so the default cozy-default BackupClass works without per-app S3 credentials — the backup driver injects the system-bucket storage onto the cluster at backup time, once the release has rendered the flag. On this flow nothing prunes the shared bucket automatically yet (a Plan carries no retention field; tracked in #4192 and #3966), so an S3 lifecycle policy on cozy-backups is a prerequisite for enabling it, not a suggestion. useSystemBucket requires backup.enabled=true; the chart refuses the combination with backups disabled at render time, and the schema carries the same rule as a marker until the apiserver enforces CEL (#2657). A system-bucket dump still running after 24 hours fails its BackupJob with the operator backup left in place. A MongoDB restore now adopts the target cluster's S3 credentials only for a storage on the same bucket as the archive (it used to adopt them unconditionally); a legacy restore into a target on a different bucket that relied on one key pair covering both must keep or re-create the source release's -s3-creds Secret by name, and is held at Ready=False RestoreCredentialsMissing until it exists.
```


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

* **New Features**
  * Added opt-in system bucket mode for MongoDB backups.
* Automatically injects platform bucket storage without
application-managed S3 credentials.
* Supports endpoint, region, path prefix, addressing, and TLS settings.
* Manages shared-bucket backup artifacts when system bucket backups are
deleted.

* **Bug Fixes**
* Improved restore credential selection to avoid cross-bucket
mismatches.
  * Preserved backup metadata when storage details are unavailable.

* **Documentation**
* Documented configuration, scheduling, retention, PITR limitations, and
restore guidance.

* **Validation**
* Prevented incompatible bootstrap and system bucket restore
configurations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 18:10:56 +03:00
Mattia Eleuteri e510d49b3d feat(monitoring): re-render node agents when a monitoring label changes
The node fluent-bit builds its tenant routes from a lookup of namespace
labels, and helm-controller upgrades a release only when its chart or
values digest changes. A tenant turning monitoring on therefore got no
route, and a removed one kept its route, until an unrelated change
upgraded monitoring-agents.

The reconciler digests the label across all namespaces and requests a
forced upgrade of the release when the digest moves. It digests every
labelled namespace rather than repeating the chart's selection, so it
cannot drift from the chart; the cost is an occasional upgrade that
renders the same config.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-09-29 17:10:55 +02:00
Mattia Eleuteri 67bf9ca317 fix(postgres): read the live Cluster when deciding the WAL-archive prefix
applyClusterPluginBackup preserves the serverName already on the Cluster, so
that a backup does not flip the archive prefix mid-stream. It read that value
through the manager's cached client, and this controller registers
For(&BackupJob{}) and nothing else, so a Cluster only reaches the cache
through a lazily started informer with no resync tied to its own changes.

An in-place RestoreJob deletes the Cluster and the chart re-renders it with
bootstrap.newServerName seconds later. A read served during that window
describes the object that was purged, so the source's serverName is written
back over the freshly rendered one and the restored cluster archives onto the
source's WAL prefix. barman-cloud-check-wal-archive then refuses to start with
"Expected empty archive" and the RestoreJob ends on its deadline -- the
failure newServerName was added to prevent.

The stale read carries a stale UID too, and that UID owns the ObjectStore this
function applies, so the store is garbage-collected as soon as it is written.

Reading through the dynamic client keeps the preservation rule and makes the
value it rests on truthful.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-09-29 17:06:12 +02:00
Aleksei Sviridkin d9ed566326 fix(migration): restore the class reclaim policy on imported volumes (#4480)
<!-- 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 VM import handoff sets the transferred `PersistentVolume` to
`Retain` so the data survives the swap from the Forklift-populated claim
to the VMDisk's claim, and then never gives the policy back. Every
imported disk therefore ends up with `persistentVolumeReclaimPolicy:
Retain`, while its StorageClass says `Delete` and every VMDisk created
directly carries `Delete`. When the tenant later deletes an imported
VMDisk through the normal path, its DataVolume and claim go away, but
the volume stays `Released` with its backing storage: an orphaned,
billed volume that no tenant can see or remove.

#### How it was measured

On a lab cluster with a LINSTOR-backed `replicated` class
(`reclaimPolicy: Delete`): a 16 GiB VM imported from vSphere produced a
VMDisk whose PV was `Retain`, while every non-imported VMDisk's PV in
the cluster was `Delete`. Deleting the VMInstance and then the VMDisk
removed the DataVolume and the claim and left the PV `Released`, still
`Retain`, with its LINSTOR resource in place. Patching that PV back to
`Delete` made the CSI provisioner delete both the PV and the LINSTOR
resource, which confirms the reclaim policy is the only thing keeping
it.

#### The fix

`Retain` is now held only for the window it protects. A new step,
`settleReclaimPolicy`, restores the policy the volume's StorageClass
declares (`Delete` when the class names none or no longer exists) once
the handoff is provably over, and until then reports "not yet" and
touches nothing:

- the replacement claim is `Bound` and the PV is `Bound` with a
`claimRef` naming that claim by UID, as the PV controller reports it
rather than as the controller wrote it, so no release is in flight;
- the transferred claim is gone;
- the claim's one and only owner is the VMDisk's DataVolume, as a
controller reference whose UID matches the live DataVolume (CDI's
adoption), so the only deletion that can reach the volume is the
VMDisk's own;
- the VMDisk exists and carries this task's output markers.

`fulfill` runs this step after adopting every disk and before creating
the VMInstance and deleting the scaffolding VM, so a VM is reported
`Succeeded` only once its volumes carry their class's policy, and an
import that fails after this point still leaves disks that clean up like
any other. The step is a compare-and-set, so repeated passes and
restarts are no-ops once settled. It also runs on the path that recovers
outputs whose status was never written, for tasks caught mid-handoff by
an upgrade. The comment that called permanent `Retain` "part of the
contract" and the matching paragraph in
`packages/system/migration-controller/README.md` are updated; the README
also says how to fix volumes left on `Retain` by imports completed
before this change, since those tasks are terminal and are not
reconciled again.

In practice a VM now stays in `Creating` for one more requeue (about 20
s) after the swap, while the PV controller rebinds and CDI adopts the
claim.

#### Tests

- `TestSettleRestoresTheClassReclaimPolicy`: a finished handoff gets the
class's policy back: `Delete`, `Retain` when the class asks for it, and
`Delete` when the class names none or is gone.
- `TestSettleIsIdempotent`: a second pass over a settled volume reports
settled and does not rewrite the PV.
- `TestSettleKeepsRetainWhileTheHandoffIsOpen`: thirteen cases, each
breaking one condition (claim not created by this import, claim not
bound, PV still `Released`, `claimRef` still on the old claim,
`claimRef` on the new claim with a stale UID, transferred claim still
present, claim not yet adopted by CDI, an extra owner, a DataVolume
owner that is not the controller, an owner reference to a previous
DataVolume, DataVolume missing, VMDisk missing, VMDisk not created by
this import). Every one keeps `Retain`.
- `TestFulfillRestoresTheReclaimPolicyBeforeFinishing`: behavioural
regression over `fulfill`. The first pass swaps the volume, keeps
`Retain`, creates no VMInstance and keeps the scaffolding VM. After
simulating the PV controller and CDI, the second pass restores `Delete`,
creates the VMInstance and removes the scaffolding VM. With the new step
turned into a no-op, this test fails.
- `TestFulfillSettlesReclaimedOutputs`: the recovery path for outputs
whose status was never written. With the handoff still open the volume
keeps `Retain`; once it is finished the policy goes back to `Delete`.
- The existing `adoptVolume` tests still assert `Retain` during the
swap.

`go test ./internal/migrationcontroller/...` and `make test-controllers`
pass, `go vet` is clean, and `make generate` leaves the tree unchanged.


### 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:

The diff touches only `internal/migrationcontroller/`,
`packages/system/migration-controller/README.md` and a comment in
`packages/system/migration-controller/templates/rbac.yaml`, which match
no entry of the trigger map; the website's VMware migration page does
not describe the reclaim policy, so it needs no change.

### 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(migration): disks imported from VMware no longer keep a Retain reclaim policy after the import. The volume gets its StorageClass policy back once it is bound to its VMDisk, so deleting an imported VMDisk deletes its volume instead of leaving an orphaned Released PersistentVolume. Volumes imported before this fix keep Retain and have to be patched back by hand.
```


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

* **Bug Fixes**
* Imported volumes are protected during claim transfers, then returned
to their StorageClass reclaim policy after the replacement claim and
volume are safely adopted. If the class or policy is unavailable, the
policy defaults to `Delete`.
* VM imports wait for volume adoption and reclaim-policy settlement
before completing, helping prevent premature volume deletion and
incomplete imports. Previously unrecorded handoff outputs are also
settled before being returned.
* **Documentation**
* Clarified reclaim-policy behavior during imports and how to restore
cleanup behavior for volumes from older imports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 14:41:04 +03:00
Aleksei Sviridkin 717d091970 fix(backups, postgres): start a Postgres restore from the backup it names (#4443)
## What this PR does

A Postgres `RestoreJob` now starts from the base backup its `Backup`
refers to, and the PITR documentation describes the window the recovery
actually has.

**Starting from the named backup.** Without `recoveryTarget.backupID`
the barman-cloud plugin picks the base backup itself
(`internal/cnpgi/restore/restore.go` in plugin-barman-cloud, then
`FindBackupInfo` in barman-cloud `pkg/catalog`): the newest backup of
the catalog when no `recoveryTime` is set, otherwise the newest one
whose end is not after `recoveryTime`, on any timeline since no
`targetTLI` is passed. A `RestoreJob` naming an older `Backup` therefore
started from another one, and on an archive that holds sibling timelines
— left by in-place restores that archived back under the source
`serverName`, before restored clusters got a fresh `newServerName` — it
could start from another branch and bring back that branch's data.
`FindBackupInfo` gives `backupID` precedence over time-based selection,
and CNPG's webhook does not count it among the mutually exclusive
targets, so it combines with `targetTime`.

- on restore the CNPG driver reads barman's `backupId` and the backup's
`stoppedAt` from the `Backup`'s `cnpg.io/Backup` and sets the new chart
value `bootstrap.backupID`, rendered as
`bootstrap.recovery.recoveryTarget.backupID`;
- it does not pin for a `recoveryTime` earlier than one second past
`stoppedAt`: the named backup cannot serve an instant before it is
consistent, and `stoppedAt` is truncated to the second while the catalog
compares barman's end time to the microsecond. Those requests keep the
plugin's time-based choice, as today;
- it does not pin either when the `cnpg.io/Backup` is gone.
plugin-barman-cloud's retention deletes it together with the base backup
it describes (`deleteBackupsNotInCatalog`), and the driver deletes one
only with its artifact, so a missing one means the catalog has lost that
base backup: pinning it would purge the target and then fail the
recovery. The plugin's choice recovers to the same end of the archive,
and the `RestoreJob` gets a `BaseBackupGone` warning event. A base
backup deleted from the object store by other means is still pinned for
as long as its `cnpg.io/Backup` stays, and that restore fails after the
purge; plugin-barman-cloud deletes such a `cnpg.io/Backup` on its next
maintenance pass (`deleteBackupsNotInCatalog`, every
`retentionPolicyIntervalSeconds`, retention policy or not), but only
from the primary of the cluster that took it, under the same UID.
Telling it apart earlier would need reading the barman catalog, which
the driver does not do; the documentation says so, and that deleting the
`cnpg.io/Backup` by hand is the way out;
- a re-restore into the same target clears a `backupID` left by a
previous one.

**Documentation** (`docs/operations/backup-classes.md`, PostgreSQL
PITR):

- the upper bound of the window is the last transaction (commit or
abort) in the archived WAL, not the newest segment: recovery ends just
before the first transaction after the target, and a segment switch or a
base backup is not one. On a database at rest every instant after its
last write fails with `recovery ended before configured recovery target
was reached`, including "at/before the most recent completed backup",
which the page recommended as the safe choice; the state as of the last
write is obtained by omitting `recoveryTime`;
- aiming at a backup's `stoppedAt` misses that backup (the plugin keeps
only a backup whose end is not after the target), falling back to an
older one or to `no target backup found`; the page now says to aim one
second past it;
- when a base backup removed from the bucket outside retention is still
pinned, and how to unpin it;
- how to read the upper bound from the archive, what pinning the base
backup changes, and a section on archives with several timelines:
PostgreSQL follows `recovery_target_timeline: latest` from the backup's
timeline through the consecutively numbered history files it can find,
so a missing `.history` confines the replay to the backup's own timeline
and the bound is the last transaction on that timeline, while a targeted
timeline that does not descend from the backup stops recovery with
`requested timeline N is not a child of this server's history`.

**Tests.** `TestCNPGRestoreBackupID` (the pinning rule),
`TestReconcileCNPGRestore_PinsTheRequestedBackup` (drives a restore
through the purge and reads back the patched `Postgres` app, including a
`cnpg.io/Backup` removed by retention, with and without `recoveryTime`),
`TestBuildPostgresAppRestorePatch_ReplacesBackupID`, a deep-copy test
for the new `stoppedAt` field, and three `helm unittest` cases for
`recoveryTarget`. With the rule disabled, 4 of the rule cases and 3 of
the reconcile cases fail; with the template change removed, 2 chart
cases fail; the retention case fails against the first version of this
PR, which pinned an id recorded on the artifact; the deep-copy test
fails once `StoppedAt` is dropped from the generated `DeepCopyInto`. `go
test ./internal/backupcontroller/...` and `helm unittest
packages/apps/postgres` (139/139, after rebasing on main) pass. Not run:
the e2e suite (`hack/e2e-chainsaw`), and a restore against a live
cluster with this driver.

### Screenshots

No UI change.

### 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/issues/716.
`content/en/docs/next/operations/services/backup-classes.md` (and
`v1.6/`) carries a copy of the corrected "The recoverable window" and
"Discovering the earliest / latest restorable time" sections; the window
and `stoppedAt` corrections apply to released versions too, the
`backupID` part only once this is released.
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up: none expected. The diff adds `bootstrap.backupID` to the
postgres `values.schema.json`, but the provider does not manage the
`bootstrap` block (`internal/provider/postgresql_schema.go`: "bootstrap
blocks use server defaults"), and the `RestoreJob` CRD is unchanged.
Left unticked for a maintainer to confirm.
- [ ]
[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(backups): a Postgres RestoreJob now starts from the base backup its Backup refers to (recoveryTarget.backupID) instead of whichever backup the barman-cloud plugin picks, which on an archive with several timelines could restore another branch's data; a recoveryTime before that backup's end keeps the previous time-based choice. The PITR documentation now bounds the recoverable window by the last archived transaction rather than the newest WAL segment.
```


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

* **New Features**
* Restores can target a specific base backup and optionally replay to a
requested recovery time. If no backup is specified, the newest eligible
backup is selected automatically.
* **Bug Fixes**
* Restore handling warns when the referenced base backup is unavailable
and allows the plugin to select a backup from the catalog.
* **Documentation**
* Clarified recovery-window boundaries, timestamp precision, idle
databases, timeline branches, and behavior when a requested target
cannot be reached or predates the earliest base backup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 14:26:49 +03:00
Mattia Eleuteri 28e42fbe4d fix(backups): match a Velero restore in its Backup's API group
The Velero driver resolves a restore's target from the Backup's
applicationRef, overriding only the kind and name, and restores the
Backup's resources. A targetApplicationRef.apiGroup never reaches it.
The restore gate honoured that group all the same, so a Velero
RestoreJob naming another group let a BackupJob of the application
actually being restored start against it, and held one of an
application the restore never touches.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-09-29 09:38:36 +02:00
Mattia Eleuteri 497dd6fc4f fix(backups): start a Postgres restore from the backup it names
Without recoveryTarget.backupID the barman-cloud plugin chooses the
base backup itself: the newest one in the catalog when no recoveryTime
is set, and the newest one ending by recoveryTime otherwise, on any
timeline since no targetTLI is passed. A RestoreJob naming an older
Backup therefore started from another one, and on an archive holding
sibling timelines - left by in-place restores that archived back under
the source serverName - from another branch, bringing back that
branch's data.

The restore reads barman's backupId and the backup's stoppedAt from
the live cnpg.io/Backup behind the Backup, and passes the id through
the chart as bootstrap.recovery.recoveryTarget.backupID. The plugin
gives backupID precedence over time-based selection, and CNPG does not
count it among the mutually exclusive targets, so it combines with
targetTime.

Two cases keep the plugin's choice. A recoveryTime earlier than one
second past stoppedAt: the named backup cannot serve an instant before
it is consistent, and stoppedAt is truncated to the second while
barman compares its end time to the microsecond. And a cnpg.io/Backup
that is gone: plugin-barman-cloud's retention deletes it together with
the base backup it describes, and the driver deletes one only with its
artifact, so pinning its id would purge the target and then fail the
recovery on every retry. That restore records a BaseBackupGone warning
event instead.

A re-restore into the same target clears a backupID left by a previous
one.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-09-29 09:38:11 +02:00
Mattia Eleuteri c6a4933cf6 test(migration): cover the recovery settle and the handoff guards
Recovering outputs that no status recorded goes through fulfill's
vm == nil branch, which settles the reclaim policy of volumes an older
controller left on Retain. Nothing drove that branch, so the write path
could be removed with the suite still green. A fulfill test now runs it
with the handoff open and finished.

The guards that stop the controller from rewriting the policy of a
volume it did not produce, or before CDI controls the claim, get one
case each: a claim or VMDisk marked by another import, a non-controller
DataVolume owner reference, and a claimRef naming the replacement claim
under a stale UID.

The storageclasses RBAC comment now also names the reclaim policy read.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-09-29 09:37:25 +02:00
Andrey Kolkov 6555ae16b8 fix(mongodb): start a backup only once the operator has pushed the storage into pbm
The wait after a storage injection watched the cluster's spec, which is
not what pbm writes to. At the pinned operator version the backup
reconciler hands pbm only a storage name and records status.s3 from the
spec entry it read, while the storage pbm uses is whatever the cluster
reconciler last pushed into pbm's own config; that push flips PBMReady
False and back to True and the reconcile stamps observedGeneration when
it ends. A backup started between the apply and that push dumped to the
previous coordinates while its Backup recorded the new ones, so both its
restore and its prune pointed at the wrong bucket. The wait was also no
wait at all: the status write that named it re-queued the job at once,
and the live read always showed the apply. The mint now waits for
observedGeneration to reach the cluster's generation and for PBMReady
to be True, and fails at the backup deadline if the operator never gets
there.

The strategy, app and cluster reads still ran ahead of the operator CR
lookup and were bounded by the backup deadline, so a strategy absent for
one reconcile 40 minutes into a streaming dump failed the job and left
the finalized CR unreachable. The CR is resolved first now; with one in
hand the cluster is not read at all, and the reads the artifact needs
are bounded by the running ceiling, past which the job fails naming the
CR left in place.

The settle check treated a field the entry lacked as agreement, so a
strategy that newly set forcePathStyle minted in the same pass as the
apply that changed addressing. A missing field now counts as its zero
value, which keeps an entry the operator rewrote without an empty region
settled while catching a value the apply adds; the storage type is
compared outright.

The runbook still said the finalizer keeps a Plan from accumulating
objects, the RBAC comment still said the timeout cancel runs on either
flow, and four wait messages read a duration as a delay. All corrected;
the runbook also describes the pbm wait.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-29 11:02:57 +04:00
Andrey Kolkov 841933dcec docs(mongodb): stop saying a Plan prunes the shared bucket
A Plan creates BackupJobs and never deletes a Backup, and PlanSpec has
no retention field, so nothing on the platform bounds a MongoDB archive
on the shared bucket today except an S3 lifecycle policy or a manual
Backup delete. The runbook's tuning section said a Plan does, two
hundred lines after the same page said a Plan carries no retention, and
three driver comments named "Plan retention" as the path that reclaims
the bucket. An operator reading the first sentence would skip the
lifecycle policy the release note calls a prerequisite. All of them now
say what deletes a Backup today, and that the finalizer frees the object
whenever one is deleted, whatever does the deleting.

The settle check after an apply compared only the fields the desired
entry carries, so an entry written with forcePathStyle while the strategy
still set it counted as settled once the strategy dropped the field, and
the apply that removed it minted in the same pass. The check now treats
a field the apply is about to remove as a change too.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-29 10:14:44 +04:00
Andrey Kolkov 707e38d17d fix(mongodb): keep every mint-time hold off a job that already has its CR
The injection and the ownership switch were gated on the job having no
operator CR yet; the precondition between them was not, and neither was
the pair of holds ahead of them. Each of those fails the job at the
30-minute deadline, so a tenant disabling backups, or the platform
dropping the strategy's s3 block, while a system-bucket dump streamed
failed the BackupJob at 30 minutes with the CR left finalized and no
Backup object to reach it, bypassing the 24-hour ceiling that exists so
a shared-bucket archive is never stranded. Everything up to the mint now
runs only while there is no CR; a job with one goes straight to the
state switch.

The settle wait after the apply was keyed on the storage key being
present, so a change to the strategy coordinates rewrote an entry the
operator had cached and minted in the same pass, the case the wait
exists for. It is now keyed on the entry's content: only an apply that
changes what the entry says costs a poll.

The mint-time reads (the render's tasks and pitr, the storage entry)
came from the manager's cache, which can lag a re-render back to the
legacy flow by one reconcile and show the flag true with the tasks not
yet visible. With a dynamic client they now come from the apiserver,
the way the deadline cancel already reads the operator CR.

Tests age the existing-CR case past the deadline and vary the three
holds, pin the coordinate-change wait, the live read and the term that
keeps the storage-race retry from swallowing an unrelated operator
error. The runbook's tuning section no longer summarises the defaults
as 30-day retention: only the CNPG and Velero strategies carry one.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-29 10:14:44 +04:00
Andrey Kolkov 46a8bd7363 fix(mongodb): apply the storage once per job, and pin the terms that decide ownership
The system-bucket storage was applied on every reconcile of a job, not
once per job: with the operator CR already minted nothing returned before
the apply, so a streaming dump saw an SSA patch every poll for up to a
day, and the only thing keeping that patch off a cluster that had
re-acquired the chart's tasks and pitr since the mint was a predicate
term no test held. The storage a CR names is fixed for its dump, so the
apply now runs only before the CR exists; the legacy-render hold covers
that window, and a test pins that an existing CR is never injected over.

The other predicate that decides whether a Backup owns an archive
compared the live entry's bucket and credentialsSecret against the
strategy, and only the bucket half had a fixture. The credential is what
tells a tenant's entry from the platform's when a bucket name collides,
so a case varying only that term joins the foreign-bucket one. The
cleanup shortcut's snapshot-kind term and the 24h running ceiling are
pinned the same way, the latter as an absolute boundary pair.

The generated field description said the schema rejects useSystemBucket
without backup.enabled. The aggregated apiserver does not evaluate CEL on
application schemas yet, so the render-time refusal is the one that
holds; the description and the runbook now say so. The runbook's restore
paragraph still described the unconditional credential swap this branch
narrowed, exempted MariaDB from a retention warning its shipped strategy
does not earn, and pointed at a Plan retention field that does not
exist; the example README and a dispatcher test's comment still carried
the pre-branch claim that the driver never injects and never deletes.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-29 10:14:44 +04:00
Andrey Kolkov a6bb0556fc fix(mongodb): own archives by where the storage points, and bound the open loops
The prune finalizer on a driver-minted PerconaServerMongoDBBackup means
the platform owns the archive: cleanup deletes the operator CR on that
marker and the operator removes the object from storage. It was stamped
from the app's useSystemBucket flag, which says nothing about where the
storage the CR names actually points. With a strategy carrying no s3
block nothing was injected, the presence-only precondition passed on a
legacy tenant-bucket entry, and the CR went out finalized against the
tenant's own bucket; deleting the Cozystack Backup then deleted the
tenant's archive. The finalizer now follows the storage's coordinates as
read back from the live cluster after the injection, and a strategy
without s3 coordinates, or a storage pointing anywhere else, holds the
job instead of minting.

The injection keyed on the same desired value. The app CR is projected
from the HelmRelease's spec.values, so the flag reads true before
helm-controller renders the revision, while the tenant's own storage,
scheduled tasks and PITR stream stay on the cluster. Injecting over that
entry redirected the tenant's nightly dump and oplog into the shared
bucket under platform credentials. A cluster still carrying tasks or
pitr is now left alone and the job held until the release renders the
flag, and the schema carries a rule marking useSystemBucket without
backup.enabled invalid, the pair the chart already refuses at render.

The storage-race retry counted surviving operator CRs against its
budget, but an errored CR is reaped the moment it is deleted (the
operator drops delete-backup without pruning on a non-ready CR), so the
count never left one and the job minted a CR per poll for the whole
deadline. The count now lives on the BackupJob and survives the reap.

A system-bucket dump in running had no bound at all: a wedged pbm agent
left the job Running forever and a nightly Plan stacked another beside
it. It now fails after 24 hours with the operator CR left in place as
the handle to whatever pbm still writes. The deadline cancel pins its
delete to the resourceVersion the live read answered for, and the
artifact-retry failure names the window that was actually in force.

Tests pin the guards that were exercised only in the harmless direction:
a system-bucket snapshot reaching the prune, the resolved prefix reaching
the cluster, a missing live client deferring the cancel, and the
finalizer refused against foreign coordinates.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-29 10:14:35 +04:00
Mattia Eleuteri af8cf743cd fix(backups): hold a BackupJob while its application is being restored
Nothing on the backup path looks at restores. A Plan creates its
BackupJobs on schedule, and the BackupJob reconciler dispatches them to
their driver whatever else is happening to the application. For CNPG that
is not merely a backup of a half-restored state: every BackupJob reconcile
re-applies the Cluster's barman plugin, and on a cluster under an in-place
restore it can land after the chart has re-rendered the Cluster with its
new serverName and put it back on the source's. The restored cluster then
archives onto the source's WAL prefix and its recovery fails with "WAL
archive check failed ... Expected empty archive". This was reproduced
with a backup triggered a few seconds after an in-place restore, and an
hourly plan overlaps a restore of up to thirty minutes about half the
time.

A BackupJob that has not started yet now waits while a non-terminal
RestoreJob in its namespace writes into the same application, resolved
as the drivers resolve it: the Backup's applicationRef, with the target's
kind and name when a targetApplicationRef is set, and the target's API
group only when it names one. A RestoreJob whose Backup is gone holds
nothing, since the restore controller fails it on its next pass.
options.targetNamespace is honoured only for a Backup taken by the Velero
strategy, the one driver that reads it; the CNPG and Job-based drivers
ignore the option and restore in place, so their restores hold whatever
it says.

The held job stays Pending with a Ready=False RestoreInProgress condition
naming the restore and is requeued, rather than failed, so a scheduled run
still happens once the restore ends. A failed write of that status is
returned, so the manager retries it. When the restore ends the condition is
removed before the job starts, since it would otherwise read "waiting"
until the driver next writes Ready, which for a healthy run is when the
backup completes. A run that has already started is left alone: its driver
owns state the gate would strand.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-09-28 19:29:22 +02:00
Aleksei Sviridkin 2c2c0301f7 chore(deps): roll up pending Renovate updates and move to Go 1.27.1 (#4447)
## What this PR does

This replaces the open Renovate PRs with one rollup. Go modules move to
their newest releases that keep the Kubernetes libraries and
controller-runtime where they are, the toolchain and builder images move
to Go 1.27.1, and the first-party code gets the Go 1.27 modernizers.
Tests for the code the bumps touch come first, in their own commits, and
pass on both the old and the new dependencies.

Before the bumps:

- The e2e sandbox image builds again: dl.min.io answers 410 for mc since
MinIO archived it, so mc now comes from its last GitHub release with a
pinned checksum.
- Renovate stops offering tags of the apimachinery fork. They mirror
upstream, so #2999 is a downgrade that drops our patch.
- `make unit-tests` now runs the tests of the nested Go modules
(kubevirt-csi-driver, token-proxy, kubeovn-webhook, api/apps/v1alpha1).
They never ran in CI before.
- New tests: JWT verification in the dashboard token proxy, the kube-ovn
plunger metrics, cert-manager object idempotency in the tenant gateway,
values-file layering in the operator, and the kubevirt CSI driver
wrapper.
- One real fix. The lineage webhook decoded admitted objects with
encoding/json, so an int64 above 2^53 came back rounded. jsonpatch v2.5
compares numbers exactly and would emit a replace that rewrites such a
field in the pod. The webhook now decodes with apimachinery's json.

Held back on purpose, with the reasons in the commit messages:

- Kubernetes libraries stay on v0.35. #3001 needs the apimachinery fork
rebased onto release-1.37 first.
- cel-go v0.29.2 and etcd v3.6.15, the lines k8s.io/apiserver v0.35 is
built and tested with.
- grpc v1.83.2. v1.84.0 is affected by GO-2026-6443 and no v1.84 patch
release carries the fix yet.
- kubevirt.io/api and the CDI API stay on what upstream csi-driver
builds against. From CDI v1.60 it no longer compiles.
- api/apps/v1alpha1 keeps go 1.26.4, so consumers of the published types
are not forced onto Go 1.27.
- go fix runs without embedlit (Go 1.27-only syntax, breaks backports to
release branches on Go 1.26) and omitzero (would make metadata, spec and
status required in the published OpenAPI schemas), and one errorsastype
rewrite that did not compile is left out.

govulncheck reports nothing reachable in any of the five modules,
standard library included.

Supersedes #4311, #4310, #4309, #3452, #3993, #3927, #3902, #3805,
#3451, #3415, #3414, #3413, #3302, #4050 and #3363. #3001 stays open.

Not checked locally: the images that build upstream projects (kube-ovn,
kamaji, cilium, linstor-csi and others) with the Go 1.27 builder. The
image build jobs of this PR cover them.

Closes #4487.

### Screenshots

No UI changes.

### 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
chore(deps): move the toolchain and builder images to Go 1.27.1 and raise Go dependencies to their latest compatible releases, closing the published advisories; the lineage webhook no longer rewrites int64 fields above 2^53 in admitted objects.
```


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

* **New Features**
  * Added a restore resource policy option for Velero strategies.
  * Added a test target to run Go tests across tracked modules.
* **Bug Fixes**
* Updated the end-to-end test environment to download and verify the
MinIO client from its current release location.
* Added coverage for token validation, webhook patching, controller
behavior, and reconciliation.
* **Maintenance**
* Updated the Go toolchain, dependencies, and container image versions.
  * Refreshed pinned workflow actions and base image digests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-26 10:17:29 +04:00
scooby87 03e2da5805 feat(postgres,mariadb)!: drop plaintext user passwords (#4078)
## What this PR does

Removes the plaintext `users[].password` field from the `apps/postgres`
and `apps/mariadb` charts (and the mariadb `root` account), so
managed-database passwords can no longer be pinned in tenant values or
committed to git. Passwords are always chart-generated into the
`<release>-credentials` Secret and preserved across reconciles via
`lookup`, matching the model `redis`/`valkey` already use. This closes
the "User passwords" item of the portal upstream epic
(aenix-org/cozyportal#1060).

The generated password never lands in the init-script Secret or the SQL
it runs: the postgres init-job reads each user's password from the
mounted `<release>-credentials` Secret at run time and applies it
through a psql variable (`ALTER ROLE … WITH PASSWORD :'pw'`), so it does
not linger in the init-script manifest a tenant can read. It does live
in cleartext `stringData` in the `<release>-credentials` Secret itself,
which is a chart manifest and therefore kept in Helm release history for
`MaxHistory` revisions like any chart-generated secret — that residue is
exactly why rotation cannot be built on the chart (see below). A
`password` or a `passwordRotation` key left over in a tenant's values is
accepted-but-inert and draws an admission warning, so an operator
editing it is told it has no effect rather than getting a silent 200.

Backup/restore into a copy keeps the target's chart-managed credentials
working: the CNPG driver clears `bootstrap.enabled` once recovery is
healthy so the init-job re-applies the generated passwords onto the
recovered roles, and the MariaDB driver excludes `mysql.global_priv`
from the dump (`ignoreGlobalPriv`) for backups captured from this
release onward, so restoring one of those into a copy does not clobber
the target's chart-managed users or `root`. Excluding the grant table
has a flip side, and it applies to every MariaDB restore, not only a
to-copy one: only accounts declared through `User` CRs (the chart's
`users` map) are captured, so an account created out of band directly in
MySQL is in no backup and a restore brings back its data and `mysql.db`
grants but not the account itself. This is also a backup-side fix only:
a restore from a backup taken **before** this change still replays the
source's `mysql.global_priv` and overwrites the target's users and
`root`, and `root` then needs an out-of-band reset (the operator applies
`rootPasswordSecretKeyRef` only at datadir bootstrap, so there is no
in-band path). See `docs/operations/backup-classes.md` for the
convergence window, the out-of-band-account limitation, and this
old-backup caveat.

Password **rotation** is deliberately **not** in this PR. A
chart-rendered credentials Secret keeps its cleartext in Helm release
history for `MaxHistory` revisions, so a rotation built on it cannot
revoke a leaked password; rotation is being reworked as a controller
that owns the Secret — design proposal in cozystack/community#72.

Migration is seamless: existing releases keep their current passwords on
upgrade (the value already lives in `<release>-credentials`, which
`lookup` re-reads and preserves); a `password` still present in a
tenant's values is ignored, so no existing HelmRelease fails validation.

Verification: `helm unittest` passes for both charts (postgres 87,
mariadb 126, including render-time username guards that reject any name
k8s would later reject as an invalid Secret key or User resource name);
`go build ./...`, `go vet`, the backup-controller tests and the
apps-apiserver tests are green; `make generate` was run with the
CI-pinned `cozyvalues-gen` v1.6.0 so `values.schema.json`, the READMEs,
the API types, and the `cozyrds` ApplicationDefinitions are all in sync
(the branch was rebased onto `main`, which reconciled cleanly with the
read-replica autoscaling feature that landed there).

### Screenshots

N/A — no UI change.

### Downstream repositories

- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up: affected — `content/en/docs/next/cozystack-api/go-types.md`
is hand-written and still shows `Password:` in the Go examples, and the
managed-app reference pages regenerate from these chart READMEs, so both
document `users[name].password` until refreshed (not yet filed).
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up: affected — the provider hand-maintains the postgres/mariadb
schemas and per-field defaults; this PR removes `users[].password`. A
follow-up issue/PR is needed there (not yet filed).

`terraform-provider-cozystack` and `website` are reached by this diff;
both boxes are left unticked because the follow-ups are not yet filed.
Same-repo future work this change suggests: the controller-owned
password rotation (design in cozystack/community#72), and extending the
"passwords only in the Secret" model to the other engines that still
accept plaintext (`clickhouse`, `mongodb`, `nats`, `opensearch`,
`rabbitmq`, `vpn`).

### Release note

```release-note
feat(postgres,mariadb)!: passwords for managed PostgreSQL and MariaDB users can no longer be set via `users[].password` — they are always auto-generated and stored only in the `<release>-credentials` Secret, never rendered into the init-script or the SQL it runs. Existing releases keep their current passwords on upgrade; a `password` (or a `passwordRotation`) left in values is ignored with an admission warning. Password rotation is deferred to a follow-up controller design (cozystack/community#72). MariaDB backups now exclude the `mysql.global_priv` grant table (`ignoreGlobalPriv`), so a restore recreates only accounts declared through the chart's `users` map — an account created out of band directly in MySQL is not captured and must be recreated after a restore.
```
2026-09-25 15:49:53 +03:00
Mattia Eleuteri 325feec805 fix(migration): restore the class reclaim policy on imported volumes
The handoff sets the transferred PersistentVolume to Retain so the data
survives the window in which the Forklift claim is deleted and claimRef
is re-pointed at the VMDisk's claim. It never gave the policy back, so
every imported disk kept Retain for good while its StorageClass says
Delete. Deleting such a VMDisk removes its DataVolume and claim through
the normal path and strands the volume Released with its backing
storage, billed and out of any tenant's reach.

Retain is now held only while it protects something. Once the new
claim and the volume both report Bound to each other, the transferred
claim is gone, and CDI has adopted the claim into the VMDisk
DataVolume as its only owner, the volume gets its class's policy back
(Delete when the class names none or no longer exists). The VMInstance
is created and the VM reported Succeeded only after that, so a task
that fails later still leaves disks that clean up like any other.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-09-25 12:07:23 +02:00
IvanHunters 0bae19fccc feat(lineage-webhook): refuse a second ApplicationDefinition for a kind
A definition declaring an application kind that another definition already
declares now fails when it is applied, instead of sitting next to the first
one where it has no effect. An update that keeps its kind is allowed, so a
duplicate that predates the check keeps upgrading, and so is a second
definition from the same Helm release, since a release that renames its
definition object creates the new one before it deletes the old one.

The webhook is registered with failurePolicy Ignore: ApplicationDefinitions
ship with the platform itself, so an outage of the webhook must never block
a bootstrap or an upgrade, and a failure to read the existing definitions
lets the request through with a warning rather than answering with an error,
which Ignore would not cover. It is the feedback layer; which definition
manages a kind is decided by the ownership rule whatever it lets through.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-09-25 09:39:35 +03:00
IvanHunters 0b7dd23b99 fix(appdef): let one ApplicationDefinition own each application kind
Nothing enforced that spec.application.kind is unique, and each consumer that
maps a kind back to a definition resolved a duplicate its own way: the
chartRef reconciler let every definition rewrite every release of its kind,
the lineage webhook and the CA projection reconciler took whichever came
first in an unordered list, and cozystack-api registered one resource per
definition. A second definition declaring a kind already in use could
therefore repoint that kind's tenant releases at its own artifact.

Add one ownership rule, the definition created first owns the kind with ties
broken by name, and route every consumer through it. A definition that
arrives later with a kind already in use no longer manages that kind's
releases whatever it is named: the chartRef reconciler skips it with a
Warning event and re-reconciles the next owner when the current one goes,
cozystack-api registers the owner only and logs the rest, its restart hash
moves when ownership moves, and the lineage and CA projection lookups
resolve the owner.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-09-25 09:39:35 +03:00
Aleksei Sviridkin 047c715775 chore: clean up stray console file and test fixtures
Drop a local kubeconfig that was committed by accident alongside the
console sources and ignore the filename so it cannot come back. Replace
environment-specific values in the vSphere migration fixtures with
synthetic ones: the tests only care about the shape of the inventory
record, not where it was captured.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-24 20:37:49 +03:00
Aleksei Sviridkin 31711685e0 fix(workloadmonitor): populate VM instance-profile label from the KubeVirt preference annotation (#4448)
## What this PR does

The WorkloadMonitor reconciler copies a VM's instance profile onto the
`workloads.cozystack.io/kubevirt-vmi-instance-profile` label of the
`Workload` object, which the billing metric pipeline consumes.
`getWorkloadMetadata` read the annotation
`kubevirt.io/cluster-instanceprofile-name`, but KubeVirt never stamps
that key: it records the cluster preference (which carries the guest OS
profile) under `kubevirt.io/cluster-preference-name`. So the profile
label was never populated, and a VM's instance profile — a Windows
guest, for example — never reached billing. The sibling instance-type
label worked only because its key,
`kubevirt.io/cluster-instancetype-name`, is the real one.

This reads the correct annotation. The emitted label name is unchanged,
so the rest of the pipeline is untouched. The wrong key appears exactly
once in the tree, at the read site, and nothing writes it;
`kubevirt.io/cluster-preference-name` is the constant
`ClusterPreferenceAnnotation` in `kubevirt.io/api` (`core/v1/types.go`),
while no `cluster-instanceprofile-name` constant exists there.

Verified on a dev cluster: with the fix, a Windows-profile VM's
virt-launcher pod (which carries `kubevirt.io/cluster-preference-name =
windows.2k22.virtio`) now yields the
`workloads.cozystack.io/kubevirt-vmi-instance-profile` label on its
`Workload`, and the profile propagates through the billing metric
pipeline into the usage report's lifetime record. A table test pins the
exact annotation key, keeps the instance-type case as a control, and
asserts the nonexistent old key yields no label.

### Downstream repositories

Walked the trigger map against the diff: this is an internal controller
label-key fix, with no change to any CRD, values.schema.json, chart
values, hack/*, namespace, variant, release asset, or telemetry metric.
No listed downstream repository is affected.

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

### Release note

```release-note
fix(workloadmonitor): populate the VM instance-profile billing label from the KubeVirt cluster-preference annotation so VM instance profiles (for example Windows) reach billing
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* Workload metadata now uses KubeVirt’s cluster preference annotation
for the instance-profile label. The unsupported cluster instance-profile
annotation is ignored.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-24 21:20:48 +04:00
Alexey Artamonov ce74fdeea9 docs(backups): describe restored credentials and pre-upgrade residue
Say that a restored copy converges its application passwords shortly
after recovery, that MariaDB backups leave out the grant table, and that
dropping the password field does not rewrite copies made before the
upgrade: old Backup snapshots and Helm revisions still hold the password
that stays live. The credentials projector comment counts only the
engines that still take a password from values.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-24 18:11:07 +03:00
Alexey Artamonov 9fd590a5a7 fix(backups): keep a MariaDB restore target's accounts
The operator ships mysql.global_priv with every logical dump, so a
restore into a copy replaced the target's grant table with the source's
password hashes: application logins failed, and root, which the operator
sets only at datadir bootstrap, stayed wrong. Set ignoreGlobalPriv on
every Backup the driver creates. An account created out of band on the
source is therefore not carried into the copy.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-24 18:11:07 +03:00
Alexey Artamonov 70dcd9daed fix(backups): let a restored Postgres app converge its credentials
A restore sets bootstrap.enabled on the target app, which skips the
init-job, so the recovered roles keep the source's password hashes while
the target's credentials Secret holds generated ones and every
application login fails. Once recovery converges, the driver records
RecoveryConverged and then clears bootstrap.enabled, so the next
HelmRelease reconcile runs the init-job and applies the passwords. Ready
carries RestoreCompletedCredentialsPending and an Event says the
convergence is still pending. A clear that keeps failing is retried
under BootstrapDisablePending within a fixed grace window after
convergence and then fails the restore. bootstrap.enabled is serialized
without omitempty so the patch sends false instead of dropping the key.

Running the init-job on a restored cluster exposes its delete pass to a
catalog the spec does not describe: the driver sets spec.databases to
the Backup's snapshot, but recovery replays WAL past it, so a database
created after the Backup comes back still marked as managed and would
be dropped with its data. The first run after a restore, told apart by
the per-RestoreJob bootstrap.newServerName that the cluster has not yet
recorded, releases such a database and its roles from management
instead, and the run records the value once every pass has succeeded.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-24 18:11:07 +03:00
Timofei Larkin ce3eb30e03 fix(api): refuse only what a name declaration actually breaks (#4429)
## What this PR does

Follow-up to #4367, taking the non-blocking findings from its approving
review. Each one is a case where a name declaration (`x-cozystack-name`)
was handled more harshly, or more quietly, than the declaration itself
warrants.

- **An unparseable schema is no longer blamed on a declaration.** A
schema that is not a JSON object has no declaration in it, and the spec
schema loader already reports it. The API server used to refuse every
create of the kind with "declares an invalid x-cozystack-name". It now
treats the schema as declaring nothing: creates keep working, and
`Kubernetes` and `Kafka` keep their legacy caps.
- **Annotation keywords no longer take a kind offline.** Any unknown key
inside a declaration used to refuse creates, so a generator adding a
`title` or an example would have done the same. JSON Schema annotation
keywords (`title`, `default`, `examples`, `deprecated`, `readOnly`,
`writeOnly`, `$comment`) and `x-` extensions are now ignored. A
constraint keyword the server does not implement, such as `format` or a
misspelled `maxLenght`, is still refused, because accepting it would
drop it silently.
- **The docs no longer overclaim.** They said a declaration no name can
satisfy is refused. That is true only for lengths: a pattern is checked
to compile, not to match anything, so `$^` rejects every name and names
the pattern as the reason. The docs now say that.
- **The API review gate sees the declarations the server refuses.** The
gate only read a declaration that was an object, and only diffed the
keywords it knew. A declaration turned into a string or null, or a
misspelled keyword, looked like no change. A changed declaration now
goes through the same parser the server uses, and the gate reports it if
it cannot be enforced. The parser moves to `pkg/apis/apps/validation` so
there is one copy of the rules.
- **The declaration no longer leaks into the published OpenAPI for
`spec`.** The whole `openAPISchema` is published as the schema of
`spec`, so `x-cozystack-name` appeared there as a vendor extension of
`spec`. It is now stripped from the published copy. `metadata.name`
still carries no limit in the published OpenAPI: it is the shared
`ObjectMeta` reference, and a per-kind copy is a bigger change than this
one.

Not changed: a pattern that can never match is still accepted. Deciding
whether a regular expression matches any string within the declared
lengths is a much larger piece of code than the mistake it would catch.

### How it is tested

- `TestUnparseableSchemaDeclaresNothing`: three non-object schemas;
creates still pass, and the Kubernetes legacy cap still holds.
- `TestNameDeclarationIgnoresAnnotations`: a declaration carrying
`title`, `examples`, `$comment` and an `x-` extension still enforces its
`maxLength`.
- `TestInvalidNameSchemaFailsClosed` gains `format` as an unsupported
constraint keyword.
- `TestClassifyNameDeclaration` gains a declaration turned into a
string, a misspelled keyword and a null declaration, all reported, and
an added annotation, which is not.
- `TestPatchSpecLeavesNameDeclarationOffSpec`: the published `spec`
drops `x-cozystack-name` and keeps an unrelated vendor extension.

### Downstream repositories

No repository is affected beyond the follow-ups already noted on #4367
(the `cozystack/website` pages on annotations and
ApplicationDefinitions). Those are still unfiled, so no box is ticked.

- [ ] 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): An ApplicationDefinition whose openAPISchema cannot be parsed no longer refuses every create of its kind as an invalid name declaration, and annotation keywords (`title`, `examples`, `x-` extensions and the like) inside `x-cozystack-name` are now ignored instead of taking the kind offline. The API review gate now reports a changed name declaration that the API server would refuse, and `x-cozystack-name` no longer appears on the published OpenAPI schema of `spec`.
```


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

## Summary by CodeRabbit

* **New Features**
* Application name declarations now support minimum and maximum lengths
and regular-expression patterns. Matching patterns are checked for
compilation; a pattern that matches no names rejects all names.
* **Bug Fixes**
* Invalid or unsupported name constraints now produce configuration
errors and block every create of the affected kind.
* Descriptive annotations and `x-` extensions are ignored when
validating name declarations.
* The custom name declaration is omitted from the published API
specification.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-24 18:46:35 +04:00
Aleksei Sviridkin 7a0aab2bdc refactor: apply the Go 1.27 modernizers
Run go fix over every first-party module: interface{} becomes any,
hand-written loops become slices, maps, min/max and range-over-int,
errors.As becomes errors.AsType, pointer helpers become new(expr) and
are inlined at their call sites, and so on. Every rewrite keeps
behaviour.

Three rewrites stay out. embedlit flattens embedded-field literals
such as ObjectMeta, syntax only Go 1.27 accepts, which would break
backports to release branches still on Go 1.26. omitzero drops
omitempty from struct fields, and openapi-gen reads omitempty as
optional, so metadata, spec and status would turn required in the
published schemas. And errorsastype rewrote an errors.As whose
target, apierrors.APIStatus, does not implement error, which
errors.AsType rejects at compile time.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-24 17:23:18 +03:00
IvanHunters 1f9bad0643 fix(workloadmonitor): populate VM instance-profile label from the KubeVirt preference annotation
getWorkloadMetadata filled the workloads.cozystack.io/kubevirt-vmi-instance-profile
label from the annotation kubevirt.io/cluster-instanceprofile-name, a key KubeVirt
never stamps. KubeVirt records the cluster preference (which carries the guest OS
profile) under kubevirt.io/cluster-preference-name, so the profile label was never
emitted and a VM's instance profile never reached the billing metric. The sibling
instance-type label worked only because its key is the real one.

Read the correct annotation. The emitted label name is unchanged, so the downstream
metric pipeline is untouched.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-09-24 17:18:06 +03:00
Aleksei Sviridkin 8f0f29e1bd test(operator): pin how component values files are layered
The first values file overwrites the component's values.yaml and each
later one merges into it; no test held that order or the strategies.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-24 16:55:03 +03:00
Aleksei Sviridkin 626e1fe19d test(gateway): pin that a second reconcile leaves cert-manager objects alone
Idempotency was pinned only for the Gateway and the HTTP01 Issuer. A
cert-manager type change that makes the rendered spec differ from the
stored one would turn every reconcile into an update. Cover the DNS01
Issuer and wildcard Certificate and the per-listener HTTP01 ones.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-24 16:55:03 +03:00
Aleksei Sviridkin 5c857c9d9f test(kube-ovn): pin the plunger's OVN cluster metrics
The metrics had no tests. Pin the series written for a cluster and its
members, and that dropping one cluster removes only its series.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-24 16:55:03 +03:00
Aleksei Sviridkin 77b8dc8935 fix(lineage-webhook): keep large integers intact when patching
With no decoder injected, the webhook decodes the admitted object with
encoding/json, which turns every number into float64, so an int64
above 2^53 comes back rounded. jsonpatch up to v2.4 compared both
sides as float64 and hid this; from v2.5 it compares json.Number, and
the webhook would emit a replace that silently rewrites such a field.
Decode with apimachinery's json, which keeps integers as int64, and
pin that the patch touches only the lineage labels and scheduling.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-24 16:55:03 +03:00
Aleksei Sviridkin 15035a915b fix(vm-instance): disable helm readiness wait on the VMInstance release (#3735)
## What this PR does

A VMInstance is served as a Flux HelmRelease that renders a KubeVirt
VirtualMachine. Since v1.5.0 (helm-controller v1.4.3 to v1.5.0) that
release is health-assessed by the kstatus poller. vm-instance sets no
`healthCheckExprs`, so the VirtualMachine goes through kstatus's own
rules in `fluxcd/cli-utils` (`pkg/kstatus/status`): `checkGeneration`
returns InProgress for any object whose `status.observedGeneration`
differs from `metadata.generation`, and past that check a `Ready=False`
condition is InProgress too, so a halted VM is never Current even with
its generation in sync. KubeVirt keeps a VirtualMachine's
`observedGeneration` behind its `generation` whenever a `spec.template`
change is pending a restart, the VM is stopped, or a live-updatable
change never converges (kubevirt/kubevirt#18700). So a healthy, Running
VM is read as permanently InProgress, the upgrade waits its full
timeout, and fails with `timeout waiting for: [VirtualMachine/...
status: 'InProgress']`. On releases still carrying the pre-1.5.0
`remediation.retries: -1` + rollback spec, the timeout then reverts the
previous manifest, dropping just-applied changes such as opened ports.

This sets `release.cozystack.io/helm-install-disable-wait: "true"` on
the vm-instance ApplicationDefinition, so cozystack-api generates the
HelmRelease with Install/Upgrade DisableWait. The release settles once
the VirtualMachine is applied, and VM health is read off the
VirtualMachine itself (`status.printableStatus` / `Ready`) and off the
WorkloadMonitor the chart already renders, which is what
`WorkloadsReady` on the Application reports. This restores the pre-1.5.0
behaviour, under which the VM was never a helm readiness gate. The same
mechanism is already used by `kubernetes-rd` and `kubernetes-nodes-rd`.

On its own that annotation does not reach a running cluster, so two gaps
in cozystack-controller are closed with it.

The first is the rollout. `applicationdefinition-controller` hashed each
definition's name and spec only, so a change confined to a
`release.cozystack.io/*` annotation left the hash unmoved and
cozystack-api kept serving the configuration it read at its last
start-up, which is the only place those annotations are ever read. The
hash now covers them too. Only that prefix, because kubectl and Helm
rewrite annotations of their own on every apply and hashing those would
roll the api Deployment on every reconcile.

The second is the releases that already exist.
`applicationdefinition-helm-reconciler` brings chartRef, valuesFrom and
labels on existing HelmReleases in line with their definition; it now
does the same for the readiness wait. Without it the annotation reaches
new instances only, which on an affected cluster is the wrong half:
#3734 reports 134 of 136 releases already looping upgrade and timeout,
and re-applying every VMInstance by hand was the only way out. The
setting is driven in both directions, since cozystack-api rebuilds the
whole spec on every write and emits `DisableWait=false` once the
annotation is gone. An absent install or upgrade block already behaves
as `DisableWait=false` and is materialised only when the definition asks
to disable the wait, so a release with nothing to change is not
rewritten.

Two alternatives were considered. A per-kind `healthCheckExprs` keyed on
`status.printableStatus` cannot work: setting it moves the
VirtualMachine from kstatus to the CEL status evaluator
(`fluxcd/pkg/runtime/cel`), which runs the same `observedGeneration`
check ahead of the expressions, and any `healthCheckExprs` added to this
kind later is a no-op while the wait is disabled. The typed
`spec.release.waitStrategy: legacy` is the narrower option, since the
legacy checker never waits on a custom resource, but it still gates the
release on the chart's Service, and with `external: true` that is a
LoadBalancer the legacy checker holds not ready until it has ingress. So
an external VM would still wait on IP allocation, which `WorkloadsReady`
already watches. Disable-wait avoids that and is the mechanism this
repository already uses for the same class of problem.

Tests: a helm-unittest suite (`tests/readiness_gate_test.yaml`) guards
that the annotation stays on the rendered ApplicationDefinition, and Go
unit tests cover both cozystack-controller paths: the hash moves on a
release annotation and stands still on an unrelated one, and the wait
lands on an existing release, is withdrawn when the annotation goes,
does not materialise empty install/upgrade blocks, does not rewrite a
release already in sync, leaves a release alone when the annotation does
not parse, and does not reach a release of another kind.

Note on existing clusters: no manual step. The definition changes on
upgrade, the config-hash rolls cozystack-api, and the helm reconciler
brings existing VMInstance releases in line. Releases that predate
v1.5.0 also carry a `remediation.retries: -1` + rollback spec, which is
left alone: with the wait disabled the upgrade no longer times out, so
that remediation no longer fires, and any write through the apps API
drops it.

Closes #3734.

### Downstream repositories

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

<!-- Walked the trigger map against the diff. The change is a readiness
annotation on an ApplicationDefinition plus two cozystack-controller
paths. It does not touch openAPISchema, kind, plural, release.prefix,
any app package, any values.schema.json or any installer/platform values
key. The website's application-definitions.md describes the config-hash
as a checksum "over the sorted definitions", which stays accurate now
that the hash also covers the release.cozystack.io/* annotations, and no
page documents those annotations today. -->

### Release note

```release-note
fix(vm-instance): VMInstance HelmReleases no longer hang InProgress or roll back healthy VMs; the release no longer waits on VirtualMachine readiness, which helm-controller's kstatus poller cannot assess correctly for KubeVirt VMs. Existing VMInstance releases are brought in line automatically on upgrade.
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* VM installations no longer wait indefinitely when Kubernetes reports
the VM’s observed generation behind its current generation.
* Changes to release settings are now detected and applied to generated
Helm releases, including install and upgrade wait behavior.
* **Tests**
  * Added coverage for VM readiness and release-setting reconciliation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-24 14:30:23 +04:00
Andrey Kolkov 61a7a257a1 fix(backups): fail Velero RestoreJob rename for non-VMInstance kinds (#4426)
## What this PR does

Fixes #3537.

A `RestoreJob` with `spec.targetApplicationRef.name` set to a new name
and `options.targetNamespace` set to another namespace reported `phase:
Succeeded` for every Velero-backed application, but only a `VMInstance`
was actually renamed. `postRestoreRename` looks up the restored
HelmRelease under the `vm-instance-` prefix, gets `NotFound` for any
other kind and returns `nil`, so a Postgres, MariaDB, ClickHouse,
MongoDB or standalone VMDisk copy came up under the **source** name and
nothing was surfaced.

Renaming those kinds through the Velero strategy cannot work by
construction: Velero DataUpload restores volumes under their original
names, and only a `VMInstance` keeps using them after its HelmRelease is
renamed (its disks keep their names and CDI adopts the restored PVCs). A
renamed database `Cluster` or `VMDisk` renders fresh volumes for the new
name and comes up empty next to orphaned source volumes.

This PR takes the short-term, correctness half of the issue: the rename
is rejected up front for every kind other than `VMInstance`, before a
new RestoreJob enters `Running` or a Velero `Restore` is created, with a
`Ready=False` message that names the kind and points at omitting
`targetApplicationRef.name` or, for kinds that have one, using the
application's engine-native strategy (for Postgres that is the CNPG
strategy, which supports restore-to-copy). The existing same-namespace
guard keeps precedence, so its message is unchanged.

A rename RestoreJob that is already `Running` when a controller with
this check starts is failed on its next reconcile, after its Velero
`Restore` may have completed. Such a job says in its message that the
application may already sit in the target namespace under its source
name (a retry without the rename would otherwise trip
`failIfTargetExists` on that copy), and its resource-modifier ConfigMap
is released immediately rather than at the job's deletion. Jobs already
`Succeeded` or `Failed` are never revisited.

Rename-on-restore for non-VM applications through engine-native recovery
(the longer-term feature in the issue) is out of scope here. A
standalone `VMDisk` is rejected as well: its rename needs a post-restore
PVC rebind and DataVolume replacement that do not exist yet, tracked in
#4428.

Tests: `TestVeleroRenameSupported` and
`TestReconcileVeleroRestore_RenameGuard`. Rejected inputs: Postgres and
VMDisk cross-namespace rename, and a VMDisk backup restored with a
`VMInstance` target kind (the guard keys on the backup's kind, since
that is what `postRestoreRename` acts on) → `Failed` with the new
message; Postgres same-namespace rename → the pre-existing DataUpload
message. Accepted inputs: VMInstance cross-namespace rename, a VMDisk
copy that keeps its name, and a VMDisk in-place restore without
`targetApplicationRef` → `Running`. The rejected cases fail on `main`
(phase `Running` instead of `Failed`); the accepted cases fail if the
`IsRenamed` conjunct is dropped.
`TestReconcileVeleroRestore_RenameGuard_InFlight` covers the
already-running case for VMDisk (`Failed`, message names the source-name
copy, ConfigMap gone) and VMInstance (`Succeeded`).

### Screenshots

N/A — no UI changes.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff: two Go files under `internal/backupcontroller/`, no API, CRD,
chart, values, schema or docs change.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
fix(backups): a Velero-strategy RestoreJob that requests a different targetApplicationRef.name for an application kind other than VMInstance (Postgres, MariaDB, ClickHouse, MongoDB, VMDisk) now fails with an explicit message instead of reporting Succeeded with the application restored under its original name.
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* Velero restores now fail with a clear message when a requested
application rename isn’t supported for that application type.
* If an unsupported rename is detected after a restore has started, the
restore is stopped and its temporary configuration is cleaned up. The
message notes that resources may already have been restored under the
original name.
* Renames for VM instances and restores that keep the original
application name continue to work.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-24 11:18:01 +04:00
Timofei Larkin 3852ba8774 ci(api-gate): report name declarations the API server would refuse
The gate read a name declaration only when it was an object, and diffed
only the constraint keywords it knew. A declaration turned into a string
or null, or a misspelled keyword, therefore looked like no change at
all. Those are exactly the inputs on which the API server refuses every
create of the kind, the harshest outcome a schema change can have.

The gate now runs a changed declaration through the same parser the
server uses and reports it when it cannot be enforced.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-09-24 09:35:10 +03:00
Timofei Larkin 146e06e75e feat(api): take Application name limits from the application's own schema (#4367)
## What this PR does

The aggregated API server capped two kinds' names by knowing what their
charts render. A `Kubernetes` cluster name was held to 32 characters
because every worker pool is a `KubernetesNodes` release named
`kubernetes-nodes-<name>-<pool>`, and the default `md0` pool has to fit
the 53-character Helm release name limit. A `Kafka` name was held to 44
because the KRaft controller pod `kafka-<name>-c-<hash>-<id>` has to fit
the 63-character DNS-1123 label limit. The arithmetic is right, but it
is the charts' knowledge living in the platform: the next application
with a sub-resource budget needs another `if r.kindName == ...` branch,
and an application served from outside this repository — the whole point
of `ApplicationDefinition` — cannot get one at all.

An application now declares the limits on its own name in its own
schema, and the server applies whatever it finds there for whatever kind
the definition describes. The chart's `values.yaml` carries a `## @name`
annotation, `cozyvalues-gen` v1.7.0 publishes it at the root of
`values.schema.json` as `x-cozystack-name`, and `hack/update-crd.sh`
carries it into the `ApplicationDefinition`:

```yaml
## @name {string} - Cluster name. Every worker pool is a separate KubernetesNodes release named `kubernetes-nodes-<name>-<pool>`, so the name must leave room for the default `md0` pool inside the 53-character Helm release name limit.
## @maxLength 32
```

```json
"x-cozystack-name": {
  "type": "string",
  "description": "Cluster name. Every worker pool is a separate KubernetesNodes release named `kubernetes-nodes-<name>-<pool>`, so the name must leave room for the default `md0` pool inside the 53-character Helm release name limit.",
  "maxLength": 32
}
```

`maxLength`, `minLength` and `pattern` are honoured; `pattern` is
unanchored, as JSON Schema specifies. A declaration only tightens:
`prefix + name` must still fit the 53-character Helm limit. A rejected
name is answered with the chart's own description, or with "limit
declared by the <Kind> application" when the chart gives none.

The schema is untrusted input — definitions from other repositories are
written by hand — so a declaration that cannot be enforced as written
(not an object, a type error, an unknown keyword such as a misspelled
`maxLenght`, `maxLength` below 1, a negative `minLength`, `minLength`
above `maxLength` or above the Helm budget, a pattern that is not RE2)
refuses every create of that kind with `configuration error: the <Kind>
ApplicationDefinition declares an invalid x-cozystack-name: …`, and logs
the same at start-up. Dropping the declaration would admit names the
chart cannot render; applying it would reject every name and blame the
user. Other kinds are unaffected, and existing objects of the affected
kind can still be read, updated and deleted.

An upgrade brings `cozystack-api` up before `kubernetes-rd` and
`kafka-rd` re-render (`kubernetes-rd` depends on `cozystack-engine`), so
the new server can briefly read a definition that predates its
declaration. For exactly that case — a definition that declares nothing
— the two previous caps stay in `legacyNameCaps` as a floor, to be
removed once no supported upgrade starts from such a definition. That is
the only per-kind knowledge left in the name checks.

Also in this PR:

- The `pre-commit` job pins `cozyvalues-gen` v1.7.0, the first release
that understands `@name`
([cozystack/cozyvalues-gen#33](https://github.com/cozystack/cozyvalues-gen/pull/33)).
- The API review gate now diffs the root `x-cozystack-name` of every
definition as `metadata.name`, so a chart that lowers its cap or adds a
pattern is reported like any other tightened field. On this PR it
reports the Kubernetes and Kafka caps as added; their effective limits
are unchanged.
- The migration 56 comment that pointed at the removed
`maxKubernetesClusterName` now points at the chart's declaration.

Not in scope, because none of it is a static limit a chart can declare:
the `Tenant` name pattern in `pkg/apis/apps/validation`, the computed
tenant-namespace length check (it depends on the parent namespace at
request time), the tenant quota hierarchy, and the Phase 2 removed-field
warning for `Kubernetes`.

### How it is tested

`TestNameCapComesFromTheShippedSchema` reads the shipped `kubernetes`
and `kafka` ApplicationDefinitions off disk, builds storage through
`NewREST` as the server does, and checks the cap and the chart's
explanation; it then rewrites `maxLength` inside the shipped schema and
checks the limit moves with it, which a cap held in code would not.
`TestInvalidNameSchemaFailsClosed` covers each malformed declaration
through the same path, `TestLegacyNameCapsCoverUndeclaredDefinitions`
pins the upgrade floor and that a declaration replaces it, and
`TestClassifyNameDeclaration` covers the gate.

### Downstream repositories

Two repositories are affected and neither follow-up is filed yet, so no
box is ticked.

- `cozystack/website` — `content/en/docs/next/development.md` documents
the `values.yaml` annotations and needs `@name`;
`content/en/docs/next/cozystack-api/application-definitions.md`
describes `openAPISchema` and needs `x-cozystack-name`.
- `cozystack/external-apps-example` — it pins no `cozyvalues-gen`
version and regenerates against whatever is current; nothing in it
declares a name constraint, so it is unaffected until it chooses to.

- [ ] 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): Application name limits now come from the application's own schema instead of being hardcoded per kind in the aggregated API server. A chart declares them with a `## @name` annotation in `values.yaml`, which cozyvalues-gen v1.7.0 publishes as `x-cozystack-name`; the API server enforces `maxLength`, `minLength` and `pattern` on `metadata.name` for any kind, answers a rejected name with the chart's own explanation, and refuses creates of a kind whose declaration it cannot enforce with a configuration error. The Kubernetes (32) and Kafka (44) name caps are unchanged in effect and are now declared by their charts.
```


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

* **New Features**
* Applications can declare minimum and maximum name lengths and
formatting patterns.
* Kubernetes cluster names are limited to 32 characters, and Kafka
cluster names to 44 characters, to meet generated resource naming
limits.
* Names that violate declared constraints are rejected with an
explanatory message. Tighter name constraints are also reported as
breaking changes.
* Declared maximum lengths cannot exceed the platform’s existing Helm
release-name budget.

* **Documentation**
* Added guidance on declaring and enforcing chart-specific resource-name
limits.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-24 10:29:27 +04:00
Andrey Kolkov d50385f310 fix(backups): tailor the Velero rename refusal to VMDisk and in-flight jobs
The refusal pointed every kind at "the application's engine-native
strategy", but VMDisk, the only non-VMInstance kind the default
BackupClass routes to Velero, has none. Offer that remedy only to kinds
that can have one.

The check also runs on every reconcile, so a rename RestoreJob that was
already Running when the controller picked up the check is failed after
its Velero Restore has completed. That job now says the application may
already sit in the target namespace under its source name (a retry that
drops the rename would otherwise trip failIfTargetExists on that copy
without explanation), and its resource-modifier ConfigMap is released
right away instead of at the job's deletion.

Assisted-By: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-23 23:53:36 +04:00
Andrey Kolkov 11bac54f93 test(backups): pin both sides of the Velero rename guard
The guard's regression cases only exercised renames, so dropping the
IsRenamed conjunct (failing every non-VMInstance restore) or keying the
check on targetApplicationRef.kind instead of the backup's kind left the
package green. Add the inputs the guard must let through, a VMDisk copy
that keeps its name and a VMDisk in-place restore without
targetApplicationRef, and a VMDisk backup restored with a VMInstance
target kind, which must still be rejected by the source kind.

Assisted-By: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-23 22:42:06 +04:00
Andrey Kolkov 88073bb209 fix(backups): fail Velero RestoreJob rename for non-VMInstance kinds
A RestoreJob that set targetApplicationRef.name to a new name and
options.targetNamespace to another namespace reported Succeeded for any
Velero-backed application, but only a VMInstance was actually renamed.
postRestoreRename looks up the HelmRelease under the vm-instance- prefix,
gets NotFound for every other kind and returns nil, so a Postgres,
MariaDB, ClickHouse, MongoDB or standalone VMDisk copy came up under the
source name with no error surfaced.

Renaming those kinds through the Velero strategy cannot work: DataUpload
restores volumes under their original names, and only a VMInstance keeps
using them after its HelmRelease is renamed, because its disks keep their
names and CDI adopts the restored PVCs. A renamed database Cluster or
VMDisk renders fresh volumes and comes up empty.

Reject the rename up front for every kind other than VMInstance, before
the RestoreJob enters Running, with a message that points at omitting
targetApplicationRef.name or using the engine-native strategy. The
existing same-namespace guard keeps precedence, so its message is
unchanged.

Closes #3537

Assisted-By: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-23 21:30:12 +04:00
IvanHunters 22f2d76ead test(controller): pin the invalid-annotation and kind-selector paths of the wait carry-over
The reconciler skips a disable-wait annotation it cannot parse, and it
selects releases by application kind. Removing either left the suite
green; each now has a test that fails without it.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-09-23 20:18:06 +03:00
IvanHunters 5f379577d5 fix(controller): carry the readiness-wait setting to existing releases
A HelmRelease created before its ApplicationDefinition carried
release.cozystack.io/helm-install-disable-wait keeps waiting on readiness,
because cozystack-api only reads that annotation while building a release
and an existing release is not rebuilt until its Application is written
through the apps API again. Where the wait is what broke, that is every
existing instance of the kind at once, each looping upgrade and timeout,
and rewriting them by hand is the operator's only way out.

The definition already drives chartRef, valuesFrom and labels on releases
that exist, so drive the wait from there too, in both directions: the API
rebuilds the whole spec on every write and emits DisableWait=false once the
annotation is gone, so a release left disabled would drift back anyway. An
absent install or upgrade block behaves as DisableWait=false already and is
only materialised when the definition asks to disable the wait, which keeps
a release nothing asks to change from being rewritten.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-09-23 20:18:06 +03:00
IvanHunters 1ed32aec58 fix(controller): roll cozystack-api when a release annotation changes
The ApplicationDefinition config hash was built from name and spec alone, so
a definition whose whole change is a release.cozystack.io/* annotation left
the hash untouched and cozystack-api kept running with the configuration it
read at its last start-up. Those annotations are only ever read there, once,
in buildResourceFromCRD, which makes the config-hash rollout the single
mechanism that delivers a change in one: without it the annotation is inert
until the api pod restarts for an unrelated reason, and an inert annotation
is indistinguishable from one that was never delivered.

Hash the release.cozystack.io/* annotations alongside the spec. Only that
prefix, because kubectl and Helm rewrite annotations of their own on every
apply and hashing those would roll the api Deployment on every reconcile.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-09-23 20:18:05 +03:00
Timofei Larkin 8f2a6bcc87 ci(api-gate): report tightened Application name limits
An application declares the limits on its own metadata.name at the
root of its schema, beside properties, where the gate's walk over spec
never looked. A chart lowering its name cap or adding a pattern rejects
names its previous release accepted, which is the same break as
tightening a spec field, so the gate reports it the same way.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-09-23 10:59:51 +03:00
Aleksei Sviridkin ce35da451f docs(api): separate the healthCheckExprs failure modes (#4387)
## What this PR does

The `healthCheckExprs` field doc said any bad expression makes the
HelmRelease hang until its timeout. That is true for some mistakes and
wrong for others, and the text ships in the CRDs, so `kubectl explain`
sends people to the wrong place.

The doc now lists the cases separately, checked against the controller
versions this repo installs (helm-controller v1.5.0, fluxcd/pkg runtime
v0.100.2, Helm v4.1.1). An expression that does not parse, an empty
`current`, or a duplicate group and kind stall the release right away
with reason `InvalidCELExpression`, and no Helm action runs. An
expression that errors on the object keeps the wait polling until the
timeout with the CEL error in the message. A `current` expression that
evaluates but never turns true also runs the timeout out, with no error
to point at it. A wrong group or kind leaves the expressions unused.

Two details go against the issue text. Reading `status` before the CR's
controller writes one errors even inside `has()`, as the issue says, but
the object is only Unknown on that poll, and the wait still succeeds
once a status shows up. A CRD that is not installed does not hang the
wait: Helm fails to build the manifest and the action fails before any
wait starts.

Doc comments and the regenerated CRD YAML only, no behaviour change.

Closes #4154

### Screenshots

No UI change.

### 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
docs(api): describe each healthCheckExprs failure mode separately in the ApplicationDefinition and PackageSource field docs.
```


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

## Summary by CodeRabbit

* **Documentation**
* Expanded the health-check expression guidance to explain how invalid
or duplicate expressions, evaluation errors, expressions that remain
false, unmatched resource types, and missing CRDs affect Helm releases.
* Clarified that health-check expressions are not evaluated when Helm
action waiting is disabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-23 11:52:49 +04:00
Aleksei Sviridkin 65a038caa4 docs(api): separate the healthCheckExprs failure modes
The field doc filed every mistake under one outcome, a HelmRelease
that hangs until its timeout. helm-controller treats them differently.
An expression that does not parse is rejected before any Helm action
and stalls the release at once, with a named reason. One that parses
but errors on the object keeps the wait polling with the CEL error
attached, and a current expression that evaluates cleanly but never
turns true runs the wait out with no error at all. A read of status
errors until the CR's controller writes one, even inside has(), but
the wait survives it. A wrong group or kind silently falls back to
the built-in readiness rules, and a CR whose CRD is missing fails the
Helm action before the wait starts.

The text ships in the CRDs and is what kubectl explain shows, so a
reader who cannot check it against the controller source was sent
to the wrong place for the parse error and the missing CRD.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-23 00:05:35 +03:00
Aleksei Sviridkin 97cb2781db test(e2e): pin the release-history read against the upstream type
The test that claimed to pin the HelmRelease v2 history shape wrote a
HelmRelease document containing the field names it expected, then read
that same document back with the same jsonpath. It established that yq
can read a fixture the test itself composed.

Its own comment described the failure it was meant to catch: a future
Flux release renames the field, the jsonpath matches nothing, the guard
reports no remediation cycle, and real cycles pass the e2e assertion. A
rename moves neither the fixture nor the expression, so the test stays
green through exactly that event. The comment also named where the truth
lives, github.com/fluxcd/helm-controller/api/v2, and pinned a copy of
the author's expectations instead.

Replace it with a Go test that supplies neither half. The expression is
read out of the shell library that ships it, and the object is built
from the upstream type at the version go.mod holds, then serialized and
matched the way kubectl does. A renamed or retyped field is a compile
error or a failed match; a guard that stops reading history fails the
lookup with a message saying so. Both directions were confirmed by
mutation.

Locating the expression by searching the file is not enough on its own.
Inline in a long script, locating it means deciding which occurrences
are code and which are prose, and every way of getting that wrong fails
green: an example quoted near a broken read satisfies the pin while the
guard reads nothing.

So the expression moves into a named assignment in remediation-guard.sh,
which the guard already sources, and the test sources that library and
takes the value the shell is left holding. Deciding which text is the
assignment is then not the test's problem: an `export`, an indent, a
second assignment later in the file all resolve the way the guard will
resolve them, because the thing resolving them is the shell.

Sourcing borrows the environment along with the shell, so the snippet
unsets the name before it sources. `go test` hands the child its own
environment, and a shell that had exported this name would answer in
place of a library that stopped assigning it: the value comes back
non-empty, the empty check stays quiet, and the pin passes over a guard
that reads nothing. That is this file's own subject reaching it through
the environment rather than through the source.

Moving the expression out of the call site puts a step between what is
pinned and what runs, so the call site is checked too, and the check
starts from the call rather than from the text: the local the guard is
handed must be assigned exactly once, by an anchored assignment whose
jsonpath is the expansion of the shared name. That a well-formed read
exists somewhere in the file is a weaker claim than it reads as, since
a second assignment to the same local, or a read filling another one,
satisfies it while the guard runs a literal nothing here pins. The
expansion and not the name: kubectl echoes a braceless template back
verbatim, so a read that dropped the `$` would return its own text,
clear the script's empty-history backstop, and carry no status the cycle
check matches. The library is checked by sourcing rather than by
reading, so an assignment the shell would run is resolved by the shell
instead of by a guess about which text the shell would pick. Each rule
is pinned by its own test, so a regression names which one it opened
rather than only that extraction broke.

How kubectl's printer treats an absent path is its own configuration
rather than part of this contract, so it is left unasserted: guessing at
it would put the expectation and the fixture back in the same hands.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-22 23:41:35 +03:00
Andrey Kolkov 8849aa13de fix(mongodb): keep a completed archive reachable and a legacy Backup unblocked
Once the operator CR is ready the archive exists, and the Backup object
is the only thing that can ever reach it again. A transient failure
creating that object (etcd timeout, quota, webhook) failed the job
terminally with no artifact, and the terminal-phase guard stopped any
retry, so the dump sat in the shared bucket with nothing to reclaim it.
The failure is now retried with backoff for a window after the dump
completed, and when it does give up the job names the operator CR that
still holds the archive.

Cleanup read the operator CR before deciding whether the Backup owned
anything, so a legacy Backup — an unconditional no-op before this driver
— could be held Terminating by a throttle or a revoked verb on a CR the
driver never owned. The snapshot records the flow, so a legacy Backup
now releases before any apiserver read; a missing or undecodable
snapshot still falls through to the read.

The opt-out refusal recognised the lingering injected entry only by the
strategy's current credentialsSecret and only while the strategy still
carried s3, so a platform change to the strategy after a tenant opted
out let a legacy CR be minted onto the platform bucket. The platform
default Secret name is now recognised as well, independent of the
strategy's current block.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-22 17:24:32 +04:00