Commit Graph
6480 Commits
Author SHA1 Message Date
Andrey Kolkov c036efaa5a test(mongodb): cover system-bucket storage injection gate and builder
Extract the two untested contracts from applyMongoDBSystemStorage into
pure functions and unit-test both, mirroring the CNPG driver's
buildBarmanObjectStore split:

- shouldInjectMongoDBSystemStorage decides whether the driver injects the
  system-bucket storage; the tests pin both branches, including the one
  that leaves a legacy app's manual bucket untouched.
- buildMongoDBSystemStorageEntry builds the {type:s3,s3:{...}} entry,
  defaulting credentialsSecret and omitting forcePathStyle when nil.

Also add a helm-unittest for the restore.yaml guard rejecting
bootstrap.enabled together with backup.useSystemBucket.

Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Assisted-By: LLM
2026-09-22 12:03:58 +04:00
Andrey Kolkov 730592f323 feat(mongodb): support the system-bucket backup flow (useSystemBucket)
MongoDB was the only cozy-default app with no useSystemBucket flow: its
backup storage was always static on the app CR (bucket/endpoint/creds
from backup.* values + 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/etc. do.

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 env secretKeyRef - the
chart cannot render them. The fix mirrors the CNPG driver: the chart adds
a useSystemBucket flag that keeps spec.backup.enabled (so the pbm agents
run) but omits the static storages/tasks/pitr and the creds Secret; the
cozy-default MongoDB strategy carries the coordinates (endpointUrl already
scheme'd by the endpoint helper, credentialsSecret cozy-backups-creds);
and the psmdb driver SSA-injects spec.backup.storages[s3-storage] onto the
live PerconaServerMongoDB at BackupJob time under its own field manager
with ForceOwnership, so a Flux re-render never reverts it. Injection is
gated on the storage being absent, so a legacy app that ships its own
static storage is left untouched. The backup snapshot falls back to the
strategy coordinates when the operator status echo is empty, so the
restore path rebuilds backupSource (credentialsSecret cozy-backups-creds
is projected into the restore namespace before dispatch).

RBAC grows patch/update on perconaservermongodbs for the injection. A
bootstrap.enabled + useSystemBucket render guard mirrors postgres.

Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Assisted-By: LLM
2026-09-22 12:03:58 +04:00
Aleksei Sviridkin cd91627c78 feat(marketplace): register a tapped repository's apps on connect (#4308)
## Problem

Connecting a marketplace repository materialized its `PackageSource` but
created no `Package`, so its `ApplicationDefinition`s never rendered and
its apps never appeared in the dashboard catalog until a manual `cozypkg
add`. The dashboard cannot drive that step: the package list it shows is
computed from the `ApplicationDefinition`s, which do not exist yet, a
chicken-and-egg that leaves a connected repository unbrowsable.

## Fix

The tap materializer creates a tap-managed registration `Package` for
each materialized `PackageSource` that has a `default` variant, the same
way the platform ships a `Package` per built-in app. Its install-marked
components (the `ApplicationDefinition` registrations) deploy on
connect, so the apps become browsable; the app components carry no
`install` block and are still instantiated per user resource, so
registration deploys no user workload.

Privileged install components are not installed without confirmation.
The check is at the install site: the Package reconciler skips a
privileged install component of a Package that still carries the
marketplace-tap label (a tap auto-registration the operator has not
confirmed) and reports `Ready=False PrivilegedNotConfirmed`, and
`resolvePrivilegedNamespaces` gates the PodSecurity label on the same
predicate. `cozypkg add --allow-privileged` sheds the label (an explicit
ownership handover), which is the confirmation; a platform Package never
carries the label. Putting the check where the install happens closes
the window a materializer-side guard would race, so the earlier
install-safety de-register machinery is removed.

Two further best-effort layers close the paths a lingering artifact or
release could take when a source revision flips a previously benign
component to privileged: `cleanupOrphanedHelmReleases` treats an
unconfirmed privileged component as not-desired and removes a
HelmRelease left from an earlier benign revision, and the PackageSource
reconciler withholds a privileged component's `ExternalArtifact` until
confirmed (re-running on confirmation through a Package watch), so the
helm-controller has no privileged content to upgrade to. A component
flipped to privileged in a new revision can still be materialized by
source-watcher before any reconciler observes the flip; closing that
race fully requires decoupling the catalog registration from install,
tracked in #4359.

`cozypkg untap` and the dashboard disconnect remove the registration
`Package` too, so the apps de-register (its registration HelmReleases
and their `ApplicationDefinition`s are garbage-collected); running
instances are left in place but unmanaged until re-tapped, the same
outcome as `cozypkg del`. Teardown deletes the Package under a UID +
ResourceVersion precondition with a re-classify-on-conflict retry, so it
never clobbers a Package a concurrent `cozypkg add` handover has just
confirmed. `cozypkg add` re-opens a tap auto-registration for variant
selection and hands it over to the user.

## Verification

- The install-site privileged gate is covered by a Package-reconciler
test (a managed registration with a privileged default variant creates
no privileged HelmRelease and does not raise the namespace; a confirmed
Package installs) and is mutation-verified.
- The two best-effort layers each have a mutation-verified test: cleanup
of a HelmRelease left when a benign component flips to privileged, and
withholding of a privileged component's `ExternalArtifact` while the
registration is unconfirmed (a non-tap platform source is never gated).
- The `cozypkg add` handover is covered by a test asserting the tap
label, the source annotation and the PackageSource ownerRef are shed and
the variant pinned.
- The teardown delete precondition is covered by a test in which a
handover lands between the read and the delete; the precondition makes
the delete conflict and the retry re-classifies and spares the user's
Package.

Earlier end-to-end on a dev cluster (predates the install-site refactor
and the layers above) with a real public demo repository
`oci://docker.io/999669/cozy-gitea:v1.0.0` confirmed the
connect→auto-register→instance→disconnect flow: `cozypkg tap` alone
registered the app within ~10s; a `Gitea` instance reached Ready and
served `/api/healthz`; `cozypkg untap` de-registered it and left the
running instance. etcd healthy, `cozystack-platform` untouched.

```release-note
Tapping a marketplace repository now registers its applications in the catalog on connect, without a manual `cozypkg add`.
```
2026-09-22 01:41:34 +04:00
IvanHunters aa20495df3 feat(marketplace): register a tapped repository's apps on connect
Connecting a marketplace repository materialized its PackageSource but created no Package, so its ApplicationDefinitions never rendered and its apps never appeared in the dashboard catalog until a manual `cozypkg add` the dashboard cannot drive (the package list is computed from the ApplicationDefinitions, which do not exist yet).

The tap materializer now creates a tap-managed registration Package for each materialized PackageSource, the way the platform ships a Package per built-in app, so the ApplicationDefinition registrations deploy on connect and the apps become browsable; the app components carry no install block and are still instantiated per user resource.

Privileged install components of an unconfirmed tap auto-registration (a Package that still carries the marketplace-tap label) are refused on several best-effort layers: the Package reconciler skips their HelmRelease and prunes a lingering one at the install site, and the PackageSource reconciler withholds their ExternalArtifact so the helm-controller has no privileged content to deploy, re-running on confirmation via a Package watch. `cozypkg add --allow-privileged` sheds the label as an explicit ownership handover, which is the confirmation; a platform Package never carries the label. A benign component flipped to privileged in a new revision can still be materialized by source-watcher before these layers observe the flip; closing that race fully requires decoupling the catalog registration from install and is tracked in issue #4359.

`cozypkg untap` and the dashboard disconnect remove the registration Package (de-registering the apps) under a UID + ResourceVersion precondition, so a concurrent `cozypkg add` handover is never clobbered; running instances are left but unmanaged. `cozypkg add` re-opens a tap auto-registration for variant selection and hands it over. The cozystack-api ClusterRole gains delete on packages for the disconnect path.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-09-21 21:57:04 +03:00
Aleksei Sviridkin 92d2eede52 test(opensearch): separate the mount-plan rewrite from the roll onto it (#4279)
## What this is

`Refs #4259`. A diagnosis of the opensearch suite timeout plus the one
in-repo change it warrants: making the failing step say which of its two
candidates actually happened. It does **not** claim to fix the root
cause — see "What this does not do".

## What the run actually showed

The `verify-statefulset-rolled-onto-the-new-mount-plan` assert erred
with exactly two field failures, both under `status`: `(currentRevision
== updateRevision): false` and `updatedReplicas: field not found`. No
volume field appears in the error list, and `readyReplicas: 1` shows as
an unchanged line, so the pod was Ready at the deadline and nothing had
replaced it. Per e2e-testing.md ("Reading a failure diff: the actual
side is a projection"), the real cause is the error line, not the diff —
and a Chainsaw diff projects the live object onto the expected fields,
so the three `http-ca` / `http-cert` / `http-key` lines showing in the
diff are the expected tree, not proof the volumes are absent. Read that
way, the volume assertions matched: the three volumes were present on
the live template. The "volumes missing" reading is the projection trap
the doc warns against.

## Which candidate

Candidate (b): the template was rewritten onto the subPath mount plan,
and the operator has not restarted the pod onto it. This is consistent
with the source, traced against opensearch-k8s-operator v2.8.0 as
vendored:

- The chart renders the CR correctly — `generate` dropped, `secret` and
`caSecret` named — proven by
`packages/apps/opensearch/tests/tls_upgrade_test.yaml`.
- Given that CR, `pkg/reconcilers/tls.go` `handleHttp` appends exactly
the three volumes the step asserts (`http-ca` from
`opensearch-test.http-ca`, `http-key` and `http-cert` from
`opensearch-test-http-server`), and `pkg/reconcilers/cluster.go`
`reconcileNodeStatefulSet` writes that template onto the StatefulSet.
Three branches return ahead of that write — no node-pool config hash,
the `opensearch.role` recreate, and parallel recovery — but the
parallel-recovery gate is `ReadyReplicas < replicas-1`, unreachable at
one replica, so the write happens. The reconcilers that run ahead of the
cluster reconciler do not stop it either: the TLS reconciler runs first
and returns the admin-certificate path's result, empty in the ordinary
case, and `securityconfig.Reconcile` short-circuits to nil when the
security-config checksum is unchanged, which the move does not change.
So the operator does write the volumes.
- The nodePool StatefulSet is `OnDelete` (`pkg/builders/cluster.go` sets
`UpdateStrategy.Type: OnDelete` alongside `PodManagementPolicy:
OrderedReady`), so the StatefulSet controller never deletes the pod to
apply a template change: `podManagementPolicy` orders creation and
deletion during scaling, `updateStrategy` decides the update. The roll
is the operator's rolling-restart reconciler's to drive
(`pkg/reconcilers/rollingRestart.go`) — it deletes the pod itself. It
needs `status.Initialized`, it needs `ReadyReplicas == replicas` (a
Ready pod is a precondition, not the blocker), then it opens a client to
the OpenSearch API and calls `CheckClusterStatusForRestart`, which holds
the restart while the cluster is red; on yellow the gate now enables
shard allocation, waits for every data node to join, waits for shard
activity to settle, then proceeds (#4317 deleted the old
`.opensearch-observability` carve-out, and this suite builds the
operator with that patch). Any of those gates can sit indefinitely.
`status.currentRevision` never advances by itself under `OnDelete`; the
operator patches it once `updatedReplicas == replicas`, so the second
assert's own condition cannot go true unless that reconciler ran to the
end.
- The pod's startup probe is a TCP check on 9200
(`builders/cluster.go`), so "connection refused" is OpenSearch not
listening, not a probe-scheme mismatch. The plaintext `GET / HTTP/1.1`
from `curl/8.5.0` on the TLS listener is neither the startup/liveness
probe (TCP) nor the readiness probe (`curl -k https://localhost:9200`,
localhost, HTTPS) — it is external traffic to the LoadBalancer address,
a red herring for the stall.

## What this changes

The step now runs two asserts instead of one. The first waits for the
operator to rewrite the template onto the three volumes, on a
reconcile-scale budget (6m) — a failure here is the operator not writing
them (candidate a). The second waits for the roll to complete onto that
template, on the pod-lifetime budget (12m) — a failure here with the
first green is the roll not completing (candidate b): the operator never
restarting the pod, which the rolling-restart reconciler holds while
`CheckClusterStatusForRestart` reports the cluster red — on yellow the
gate enables shard allocation, waits for every data node to join, waits
for shard activity to settle, then proceeds — or a replacement that was
deleted but never came Ready. The second assert carries the three volume
projections and `observedGeneration == metadata.generation` as well, so
the roll is pinned to the template the first assert checked rather than
to whatever revision is current when the status converges. The suite
`catch` also captures the StatefulSet as YAML and as a describe, and the
operator's own log, because `updateRevision`, `currentRevision`,
`generation` and `updatedReplicas` are what separate the two halves —
describe alone omits them — and the restart decision is logged in the
operator, not in the cluster pods. The reconciler logs `Starting rolling
restart of the StatefulSet` (`rollingRestart.go:155`) once its early
gates (`Initialized`, ready-replica, update-pending) pass; it builds its
OpenSearch client just before, at `:138`, and that client pings the
cluster — so a run whose log lacks `Starting rolling restart` either had
an earlier gate hold it silently or had the client ping fail loudly, not
only the former, while that line present followed by a `Couldn't proceed
with rolling restart ... because <reason>` line names a health-stage
hold (the health gate is `CheckClusterStatusForRestart`, and it is not
the last: `PreparePodForDelete` follows it). The operator-log `tail` was
raised so it spans the whole assert window rather than a third of it.

## What this does not do

It does not fix the stall; #4317 does, and is already merged. The run
that captured this (`Run E2E tests`, job 104576486900) shows the
rolling-restart reconciler holding on health: the manager logged
`Starting rolling restart of the StatefulSet opensearch-test-nodes`
(`rollingRestart.go:155`, printed only after the `Initialized` and
ready-replica gates passed) and then, every ten seconds to the deadline,
`Couldn't proceed with rolling restart ... waiting for health to be
green` (`os_data_service.go:180`), which is reached only after
`GetFlatClusterSettings` returned 2xx (`os_client.go:224` makes anything
else an error). So the operator reached the cluster and the cluster
answered; health held the restart. #4259 established why, and the CA
move plays no part: on a single data node the bootstrap node leaving is
audited into a fresh `security-auditlog-*` index with one replica, that
replica has nowhere to go, the cluster stays yellow for good, and the
v2.8.0 restart gate opens only on green (or on a yellow
`.opensearch-observability`) so it never opens. #4317 backports
opensearch-project/opensearch-k8s-operator#1569 to fix that gate; this
PR is only the diagnostic split that names which half of the roll a
failure is. The split content itself is not the failure: on `49f3f059f`
both asserts passed, the second after 1m47s, and this change touches
only the step's comments and the operator-log tail, not the assertions.
With this change in place, a future occurrence names the half, and the
catch carries the roll's own fields and the operator log, so the
crust-gather can be read against a specific claim rather than an
ambiguous one.

### Release note

```release-note
NONE
```

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

* **Tests**
* Improved OpenSearch StatefulSet failure diagnostics by capturing YAML,
detailed resource descriptions, and the latest operator pod logs.
* Expanded rolling-restart verification to check volume projections,
template updates, rollout readiness, observed and metadata generations,
and revision consistency.
* Separated template validation from rollout completion checks with
dedicated six- and twelve-minute time limits.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 20:44:23 +04:00
Timur Tukaev e32353e839 docs(agents): correct the labeler's trigger list (#4090)
## What this PR does

The PR Title Auto-Labeling section still lists `edited` among the events
`.github/workflows/pr-labeler.yaml` runs on. That trigger was dropped in
#3211 — the workflow carries the note at the top of its `on:` block, and
the v1.6.0 changelog records the change — so the document and the
workflow have disagreed since.

I hit it the way the document's intended reader would. Three PRs opened
with GitHub's branch-derived titles, corrected afterwards to
Conventional Commits, and the labels did not follow:
`area/uncategorized` on all three and no `kind/feature`, because nothing
re-ran. The second paragraph this PR adds is that consequence, which is
the part a contributor actually needs: a corrected title does not
re-label until the next push or a reopen, and since the labeler only
ever adds, the `area/uncategorized` left by the first title stays until
a human removes it.

### Screenshots

Not applicable — no UI change.

### Downstream repositories

I walked the trigger map in `docs/agents/contributing.md` against this
diff. The only file it touches is that document, and the two triggers
pointing at it are narrower than this change: `cozystack/community`
restates the **commit convention** from it (types, scopes, sign-off, the
`Assisted-by: LLM` trailer) and the **PR template and downstream
checklist**, and `cozystack/ccp` gates its `package-bump` skill on the
same commit convention. Neither restates the labeler's event list, which
is what changes here. Nothing else in the map reaches a docs-only
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
NONE
```


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

## Summary by CodeRabbit

- **Documentation**
  - Clarified when pull request title auto-labeling runs.
  - Documented that title edits do not trigger relabeling.
- Explained that existing labels remain until a push or reopen, while
additional uncategorized labels require manual removal.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 17:25:03 +03:00
scooby87 6cd78b0e30 feat(platform): make the Keycloak realm name configurable (#4237)
## What this PR does

Fixes #4216. The Keycloak realm name `cozy` was hardcoded across every
SSO component, so operators could not rename the realm without patching
vendored templates that `make update` reverts. The kube-apiserver OIDC
issuer is already configurable out-of-band via talm `oidcIssuerUrl`,
which made the mismatch worse: pointing the apiserver at a custom realm
left the dashboard, linstor-gui and the generated kubeconfig on `cozy`,
so SSO broke with an issuer mismatch.

This adds a single `authentication.oidc.realmName` (default `cozy`) to
the platform Package values and threads it through the `_cluster`
channel so every consumer resolves the same realm: keycloak-configure
(realm, clients and `cozystack-cluster-admin` roles), the dashboard and
linstor-gui gatekeepers (issuer/login/logout URLs), the info kubeconfig,
and — beyond the components listed in the issue — the Grafana and
managed-Kubernetes apiserver OIDC issuers, which carried the same
hardcoded `/realms/cozy` in their `_helpers.tpl` and would otherwise
have left SSO half-broken.

`keycloakInternalUrl` is now derived from `realmName` when left unset,
so the realm is named in one place. Its default changed from the literal
in-cluster URL to `null` with a documented tri-state: `null` derives
`http://keycloak-http.cozy-keycloak.svc:8080/realms/<realmName>`; `""`
keeps the existing escape hatch that restores external OIDC discovery;
any explicit value is used verbatim. A stock cluster renders the
byte-identical `keycloak-internal-url` it did before, so existing
clusters are unaffected on upgrade.

The Kubernetes CR object names (`keycloakrealm-cozy`, `keycloak-cozy`)
and the `realmRef` that clients point at are internal resource names,
not the realm's technical name, and stay fixed — the EDP operator
resolves clients into the renamed realm through the unchanged
`realmRef`.

Covered by helm-unittest across all seven affected packages, written
test-first (red → green, mutation-verified): default stays `cozy`, a
custom realm reaches every URL, the derived internal URL follows
`realmName`, the `""` escape hatch survives a custom realm, and a
numeric/reserved realm name stays a string. `packages/extra/info` had no
unit-test target, so one was added.

### Downstream repositories

`cozystack/website` needs a follow-up:
`content/en/docs/next/operations/configuration/platform-package.md` is a
hand-written table of the `spec.components.platform.values.*` keys and
does not yet list `authentication.oidc.realmName`. Leaving the box
unticked for a maintainer to action rather than filing a speculative PR.

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up: document `authentication.oidc.realmName` in
`platform-package.md`
- [ ]
[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(platform): the Keycloak realm name is now configurable via `authentication.oidc.realmName` (default `cozy`), threaded consistently through the apiserver OIDC issuer, dashboard, linstor-gui, Grafana, the managed-Kubernetes apiserver and the generated kubeconfig. Set it to the same realm as the talm `oidcIssuerUrl`. The `authentication.oidc.keycloakInternalUrl` default changed from the literal in-cluster URL to `null`, which now derives `http://keycloak-http.cozy-keycloak.svc:8080/realms/<realmName>`; clusters that leave it unset render the identical URL as before, and `""` still selects external OIDC discovery.
```



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

* **New Features**
* Added configurable Keycloak realm support across OIDC integrations,
including Kubernetes, dashboards, monitoring, kubeconfig, and
authentication services.
* The realm defaults to `cozy` and can be customized through platform
authentication settings.
* Internal Keycloak URLs are derived from the configured realm when not
explicitly provided.
* Added realm-name validation and support for explicitly disabling
derived internal URLs.

* **Documentation**
* Updated OIDC guidance to describe configurable realm behavior and
defaults.

* **Tests**
* Added coverage for custom realms, defaults, validation, and generated
OIDC URLs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 17:05:34 +03:00
scooby87 6677c24beb fix(ci): fail the trailer check on an empty commit range (#4278)
## What

`hack/check-commit-trailers.sh` printed `Commit trailers OK: 0 commit(s)
checked.` and exited 0 when the range it was given resolved to zero
commits, so a run that examined nothing was indistinguishable from a
clean run. An empty range now prints a message naming the empty case and
exits 1.

## Why

The documented pre-push recipe in `docs/agents/contributing.md` is
`hack/check-commit-trailers.sh origin/main..HEAD`. The range is resolved
in the process working directory, so an agent whose shell sits in a
different checkout than the branch resolves it against the wrong `HEAD`
and gets nothing — and the old green there was a green nobody could tell
from a real one, defeating the check precisely when the checkout is
misconfigured. The CI path is unaffected:
`.github/workflows/pre-commit.yml` builds the range from a pull
request's merge base and head SHA, which GitHub refuses to open with no
commits between the two, and which goes empty only once the base has
absorbed the head's commits and there is nothing left to merge, where a
red trailer check is harmless.

## Test

TDD, bats: the previously buggy `accepts an empty range` test is
inverted into `fails on an empty range instead of reporting OK`, which
asserts a non-zero exit, that the message names the empty case, and that
the `Commit trailers OK` line is absent. Mutation-verified — removing
the guard turns it RED, restoring it turns it GREEN. Full suite: 28
passing, 0 failing.

## Release note

```release-note
Local runs of `hack/check-commit-trailers.sh` now fail on an empty commit range instead of reporting OK, so the documented pre-push check no longer passes silently when it examined nothing.
```

Fixes #4263


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

## Summary by CodeRabbit

- **Bug Fixes**
- Commit trailer checks now fail when the specified commit range
contains no commits.
  - Empty ranges no longer report a misleading successful validation.
  - Error output clearly indicates that no commits were checked.

- **Tests**
- Added coverage confirming empty commit ranges return a failure status
and appropriate diagnostic output.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 16:12:39 +03:00
myasnikovdaniil a1b32f2e5a ci(workflows): build Talos only when its sources changed (#4021)
Stacked on #4020, not independently mergeable.

Nothing on the same-repo PR path consumes either half of what
`build-talos` produces. `image-talos` pushes
`${REGISTRY}/talos:pr-N-sha`, which no chart in the tree references and
which is not even version-tagged here (`PUBLISH_VERSIONED` and
`PUBLISH_FLOATING` are both 0). `image-matchbox` stamps
`packages/extra/bootbox/images/matchbox.tag`, and that one line is the
whole content of `pr-patch-fragment-talos` - checked against the
artifact from a real run - but bootbox is opt-in and the E2E platform
Package does not enable it, so no run pulls the image. Unstamped tag
leaves installer artifact pointing at released matchbox, which is a
valid ref nothing on this path resolves.

So the job ran a privileged siderolabs imager build on every PR for two
artifacts with no reader. Now it runs when the PR touched
`packages/core/talos`, and `finalize` accepts a skipped result as well
as a successful one. Without that second half skipping it would block
finalize, and therefore e2e, on nearly every PR.

Forks keep the unconditional build, and the reason is not about talos.
`e2e-fork.yaml` publish job requires at least one OCI archive to exist,
an invariant that holds today only because this job always ran, so a
fork PR touching nothing under `packages/` would arrive with an empty
export and fail a legitimate run. Teaching that guard what `plan`
expected to build is work in the fork allowlist path and belongs in its
own change.

Saving is about 1.5-2 min of critical path. Main point is removing a
privileged image build that nobody reads from every pull request.


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

## Summary by CodeRabbit

- **CI Improvements**
- Pull request validation now runs Talos builds only when relevant
changes require them, while retaining full validation for fork-based
contributions.
- Finalization correctly handles skipped Talos builds and preserves
generated boot artifacts when builds succeed.
- Changed-file detection now accounts for renames, deletions, and shared
build inputs.

- **Documentation**
- Updated testing and build guidance to reflect the current validation
behavior.

- **Tests**
- Added comprehensive coverage for build classification, workflow
outputs, skipped jobs, and artifact handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 14:52:53 +05:00
Alexey Artamonov 7fd8376ccb fix(ci): fail the trailer check on an empty commit range
The pre-push recipe resolves origin/main..HEAD in the process working
directory, so an agent whose shell sits in a checkout at or behind the
base resolves it to zero commits and the check printed "Commit trailers
OK" with exit 0. A run that examined nothing was indistinguishable from
a clean one, defeating the recipe exactly when the checkout is wrong.
Treat an empty range as a named failure so the misconfiguration is loud;
the CI path is unaffected because it builds the range from a pull
request's merge base and head, which is empty only once nothing is left
to merge.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-21 12:20:22 +03:00
Alexey Artamonov 142a14cb69 fix(platform): reject an empty realm name instead of coercing it to cozy
realmName resolved through `| default "cozy"`, which fires on both null
and "". Unset (null) legitimately means the default, but an explicit ""
is a meaningless value and coercing it hid the mistake. Split the two:
null falls back to cozy, an empty string reaches the charset guard and is
rejected — the same null-vs-"" distinction keycloakInternalUrl already
draws a few lines below.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-21 12:15:10 +03:00
Alexey Artamonov ff637ed9a2 fix(platform): validate the realm-name charset and regenerate kubernetes artifacts
The realm-name guard checked only the type and echoed the parsed value
back, so its hint told an operator who wrote `on` to set realmName:
"true" — the token YAML had already discarded. Drop the echo and add a
slug-charset check: the name is spliced verbatim into URL path segments
and a shell token, so a "/" or a space must be rejected at the source
rather than split a URL or the hook script downstream.

The description edit in the previous commit was hand-applied to the
generated kubernetes README and never reached the Go types, leaving
pre-commit red; regenerate both with cozyvalues-gen. Finish the
oidc-tenant System-mode prose that still read cozy as a fixed name.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-21 12:15:10 +03:00
Alexey Artamonov 949096694b fix(platform): reject non-string realm names and shell-quote the hook realm
A realmName that YAML resolves to an int or a bool (a bare 123, or a
reserved word like on/off) was coerced with toString, silently renaming
the realm to something the operator never typed (on -> "true"). Fail the
render with a quoting hint at the source instead of surfacing it later as
a broken issuer URL.

The update-email hook Job interpolated the realm into its shell script
unquoted, so a name carrying a space or a shell metacharacter could break
or inject into the script; single-quote it. The Grafana and managed-cluster
issuer helpers build the URL with printf %s straight off the channel value,
so guard them with toString for the same non-string reason.

Name the configured realm in the tenant kube-apiserver oidc.mode
description and the Grafana identity-mode comment, which still read cozy as
if it were fixed.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-21 12:15:10 +03:00
Alexey Artamonov a3df02df23 fix(kubernetes): name the configured realm in the OIDC denial message
The claimValidationRules message in the System-mode AuthenticationConfiguration
hardcoded "in the cozy realm", so on a renamed realm it told a rejected user to
look in a realm that no longer exists. Build it from the same realm the issuer
uses (toString-guarded, as the derived issuer URL already is).

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-21 12:15:10 +03:00
Alexey Artamonov 80d32b3ab7 fix(keycloak): complete realm-name coverage and guard non-string names
The enable-update-email hook Job in keycloak-configure still pinned
REALM=cozy, so on a custom realmName it waited on a realm the chart never
provisions and the cold install timed out — the one seventh consumer left
behind, and the only value nobody could set without breaking the install.

A bare-integer or YAML-reserved realm name reached the printf %s that
builds the derived keycloakInternalUrl as a Go int, rendering a
"%!s(...)" format error into the URL; coerce the name to a string once
at the source.

Document that renaming the realm on a running cluster provisions a fresh
empty realm and orphans the accounts in the old one, and note in the OIDC
tenant and grafana guides that the realm name is now the configurable
default. info/Makefile carried NAME=etcd, so cozyhr drove the wrong
release from that package; correct it while touching the file.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-21 12:15:10 +03:00
Alexey Artamonov 44a9a8e735 feat(platform): make the Keycloak realm name configurable
The realm name "cozy" was hardcoded in keycloak-configure (which owns
the realm, its clients and the cozystack-cluster-admin roles), in the
dashboard and linstor-gui gatekeepers, in the info kubeconfig, and in the
Grafana and managed-Kubernetes apiserver OIDC issuers. Operators could
not rename the realm without patching vendored templates, which
`make update` reverts. The apiserver OIDC issuer is already configurable
via talm oidcIssuerUrl, so pointing it at a custom realm left every SSO
component on "cozy" and broke login with an issuer mismatch.

authentication.oidc.realmName (default "cozy") now rides the _cluster
channel so every consumer resolves the same realm, and the default
keycloakInternalUrl is derived from it so the realm is named in one place.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-21 12:15:10 +03:00
Aleksei Sviridkin 8799a4e4aa fix(cluster-api): backport live migration before host eviction (#4291)
## What this PR does

CAPK v0.1.10 handles host eviction by draining and deleting the guest
VMI. It also looks up the physical host name in the guest cluster, and a
recreated guest can remain cordoned. Backport CAPK #374, #389 and #392
onto the current provider API and runtime, with separate bounded
submission, active migration and fallback windows, and ordinary VMIM
objects without a priority, a field the pinned `kubevirt.io/api` v1.5.0
does not carry. The submitted VMIM deadline survives reconciliation;
expired, invalid or legacy marker values enter the existing fallback
path.

Add the manager build to the package pipeline, read its digest pin
through the provider imageUrl override, and ship VMIM create permission
in the compressed provider manifest. Source tests and four packaging
checks pass, including failed-build pin preservation and the rendered
ConfigMap payload. The regression tests include unprocessed submissions,
legacy and invalid deadline values, repeated reconciles, and a fresh
active migration window after submission expiry. The updated manager
also cross-compiles for linux/amd64 with Go1.26.4. The previous OCI
build predates the submission-deadline correction and does not validate
an image containing this change. The updated source still needs the
maintainer-owned image build described below.

The checked-in pin still identifies the original upstream image, so
installing this source chart directly does not enable the backport. The
regular main and release builds now include this package, stamp the
built image digest, and package that exact artifact; the release
workflow also commits the generated pin. Pre-merge E2E validation needs
a maintainer-owned build because fork publication only allows image
repositories already present in the base tree.

### Downstream repositories

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

### Release note

```release-note
fix(cluster-api): attempt live migration before drain/delete on External eviction, use the guest node name for fallback drain, and uncordon recreated guests.
```


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

- **New Features**
- Added a Cluster API Provider KubeVirt image pinned to version 0.1.10.
- Added migration-first workload eviction with fallback draining and
node recovery.
  - Added permission to create virtual machine migration resources.
  - Included the provider image in builds and deployment templates.

- **Bug Fixes**
- Preserved migration deadlines across retries and improved recovery
after interrupted or completed migrations.
  - Added a bounded wait for migrations before falling back to draining.

- **Documentation**
- Documented the provider image version, patches, build process, and
release considerations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 22:23:10 +04:00
Aleksei Sviridkin 13a2a0fd2e fix(registry): preserve write options in aggregated storage (#3937)
## What this PR does

Preserves Kubernetes write options when the application, tenant-secret,
and security-group APIs delegate create, update, and delete operations
to controller-runtime. Passing an API options object only through
`client.*Options.Raw` is insufficient because controller-runtime's
`As*Options()` methods rebuild `Raw` from the typed fields; dry-run,
field manager, field validation, delete propagation, grace periods, and
preconditions were silently lost.

The new converters populate the typed client options, and all affected
application, tenant-secret, and security-group paths use them. The
force-create PUT path preserves the caller's options as well.
TenantSecret PATCH is served through Get and Update. Backend rejections
surfaced by these writes now keep their machine-readable status on the
wire: the application storage rebuilds the `StatusError` with context
instead of flattening it through `fmt.Errorf`, so a quota or admission
rejection answers 403/409 with its reason instead of a generic 500 with
none.

Tap storage uses a different client and is outside this change.

This was reproduced on Cozystack 1.6.1: `kubectl apply --dry-run=server`
against an `apps.cozystack.io` resource performed a real write and
created the release.

Validation:

- `go test ./pkg/registry/...`: passed.
- `go test -race ./pkg/registry/...`: passed.
- `make generate`: no diff.
- `make manifests`: no diff.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map against the registry implementation and tests. No
external schema, CRD, package layout, tooling anchor, or downstream
hardcoded contract changes.

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

### Release note

```release-note
fix(registry): preserve dry-run and other Kubernetes write options in application, tenant-secret, and security-group storage operations, and keep backend error statuses on the wire
```


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

- **Bug Fixes**
- REST create, update, and delete operations now preserve request
options such as dry-run, field management, validation, deletion
propagation, and preconditions.
- Force-create and HelmRelease update requests now retain their supplied
options.
- Dry-run security group deletions now accurately report their
prospective completion state without modifying stored data.
- Delete and other REST errors now preserve their original status,
reason, and HTTP code while adding context.

- **Tests**
- Added coverage for write-option handling across applications, tenant
secrets, security groups, and shared option conversions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 22:16:45 +04:00
Aleksei Sviridkin 3219f7ee9a fix(backups): give the barman-cloud sidecar its own resources (#4220)
## What this PR does

The barman-cloud plugin injects its sidecar with no resource requests or
limits. What that means depends on the namespace. A tenant with
`resourceQuotas` set ships a `LimitRange` that defaults containers to
128Mi (`packages/apps/tenant/templates/quota.yaml`), so there the
sidecar inherits that default and is OOMKilled mid-backup. A namespace
without a `LimitRange`, which is `cozy-keycloak` and any tenant that
leaves `resourceQuotas` empty, gave the sidecar no requests and no limit
at all, so nothing reserved memory for it and nothing bounded it. The
failure on the tenant path is quiet from the control plane: the
`ObjectStore` stays healthy, the `Cluster` stays `Ready`, and only the
backup fails.

Observed on a 1.6 cluster while backing up a 38 MB database:
`lastState.terminated.reason: OOMKilled`, `exit 137`, four restarts on
the sidecar, and a cgroup high-water mark of 254 MiB.
`container_memory_working_set_bytes` peaked at only 64 MiB over the same
window — it is sampled, and it misses the spike that actually triggers
the kill, which is why the working-set number does not explain the
failure on its own.

Both paths that build an ObjectStore now carry resources:

- `cozy-lib.barman.sidecarConfiguration`, used by
`packages/apps/postgres` for the backup and recovery ObjectStores and by
`packages/system/keycloak`;
- `barmanSidecarConfiguration()` in
`internal/backupcontroller/cnpgstrategy_controller.go`, which builds the
platform's own ObjectStore on the `useSystemBucket=true` path.

Requests are 100m/256Mi with a 1Gi memory limit on every caller: 256Mi
holds the measured working set and 1Gi is four times the measurement,
which is also the ceiling a caller without a LimitRange gets where it
had none. No CPU limit is set, following the `entityOperator` precedent
in `packages/apps/kafka/templates/kafka.yaml`: a throttled sidecar
stalls WAL archiving instead of failing it, which is harder to notice
than an outright failure. Measured CPU peak was 50 mCPU. Inside a tenant
the limit is charged against the `ResourceQuota` `limits.memory` budget,
1Gi per instance pod; a 4Gi tenant spends a quarter of it per Postgres
instance.

The helper is renamed from `checksumSidecarConfiguration` to
`sidecarConfiguration`, because it no longer carries only the checksum
pin and its name and doc comment would otherwise be wrong.

Tests cover both paths and both render sites: a new helm suite in
`packages/apps/postgres`, extra assertions in the existing keycloak
suite, an assertion in the controller test, and a deepcopy test for the
new field. Each of them fails against the unfixed sources.

One thing this PR deliberately does not do. This is the fifth time the
128Mi tenant default has been worked around per component — kafka and
zookeeper presets (#2537), the kafka entity-operator (#2934), the
cert-manager cainjector (#3199), and the note carried in the
etcd-operator values. Whether the default itself should move looks like
your call rather than something to fold into a fix for one sidecar, so
it is left alone here.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff, entry by entry. The change touches one cozy-lib helper, the two
chart templates that include it, one Go function, and tests. It adds,
renames or removes no package; changes no `values.yaml`,
`values.schema.json`, `Chart.yaml` or `README.md`, so no version enum,
default or generated artifact moves; changes no `ApplicationDefinition`
semantics, no `release.prefix`, no output Secret or Service name;
touches no CRD, no namespace, no `hack/` file, no telemetry metric or
label, no `cozy-proxy` annotation, no node or network requirement, and
no commit or PR convention. No entry matches.

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

### Release note

```release-note
fix(backups): give the barman-cloud sidecar its own resources
The barman-cloud sidecar was injected without resource requests or limits, so in tenant namespaces it inherited the 128Mi `LimitRange` default and was OOMKilled during backups while the `ObjectStore` and the `Cluster` both stayed healthy. It now requests 100m/256Mi and caps memory at 1Gi on the chart path and on the platform's Go path alike; in namespaces without a LimitRange the sidecar had no limit before and gets the same 1Gi ceiling.
```


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

- **New Features**
- Added explicit CPU and memory resource settings for barman-cloud
backup and recovery sidecars.
- Backup sidecars request 100m CPU and 256Mi memory, with a 1Gi memory
limit.
  - Recovery sidecars receive a 1Gi memory limit.
  - Preserved the S3 checksum configuration.

- **Bug Fixes**
- Prevented sidecars from inheriting unsuitable tenant resource limits.
- Ensured resource settings are preserved when backup configuration is
copied.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 22:15:36 +04:00
Aleksei Sviridkin d71e78a74a fix(linstor): wait for temporary probe devices (#4100)
## What this PR does

Backports
[LINBIT/linstor-server#528](https://github.com/LINBIT/linstor-server/pull/528)
to the LINSTOR 1.33.3 build used by piraeus-server. A ZFS probe volume
can be created before udev publishes its device path, leaving the pool
without I/O properties and blocking replica placement on 4K pools.

Wait for the probe device before reading its properties, retry up to
three times and report failures. Skip null paths in the existing volume
list and fall back to a probe when ZFS has no usable path. Serialize
probe creation and cleanup, stop after cleanup failures and preserve
interrupts. Inactive LVM volumes retain the existing no-probe behavior.

Validation: all six patches apply together to v1.33.3. All 1026 tests
and `assemble` pass on Linux amd64 with JDK 11, including 17 regression
tests; 14 of those fail without this patch. `checkstyleMain` reports the
same existing return-inside-for error at `DrbdAdm.java:970` from
`retry-secondary-after-mkfs.diff`, reproduced without this patch; no new
findings.

### Downstream repositories

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

### Release note

```release-note
fix(linstor): wait for ZFS probe devices before reading pool I/O properties to avoid incorrect block sizes and failed replica placement
```


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

## Summary by CodeRabbit

- **Bug Fixes**
- Improved storage device probing by waiting for temporary devices to
become available and retrying when necessary.
- Improved compatibility with ZFS volumes when standard device paths are
unavailable.
  - Prevented unnecessary probing of inactive shared LVM volumes.
  - Improved handling of interrupted probes and cleanup failures.

- **Documentation**
- Added documentation describing storage probing behavior and
compatibility updates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 21:50:25 +04:00
Aleksei Sviridkin ee09e73343 fix(kafka): make topics[].config optional (#3935)
## What this PR does

Makes `topics[].config` optional across the Kafka typed API, generated
schema, documentation, and chart contract. The KafkaTopic template
already omits an empty config, and Strimzi accepts the field as
optional; the Cozystack schema was the layer incorrectly rejecting such
topics.

The Helm test renders a topic without config and asserts `spec.config`
is absent. The Chainsaw fixture also omits config and waits for the
resulting KafkaTopic to reach Ready.

Go API compatibility: `Topic.Config` changes from `runtime.RawExtension`
to `*runtime.RawExtension`, so Go callers constructing that field must
pass a pointer. The Terraform provider intentionally does not model
Kafka topic tuning (`config`), so this does not require a provider
follow-up.

Validation:

- `helm unittest packages/apps/kafka`: 36/36 passed.
- `go vet ./kafka` in `api/apps/v1alpha1`: clean; the package has no
test files, so this shows it compiles.
- root `make generate`, which owns `zz_generated.deepcopy.go`, and `make
-C packages/apps/kafka generate`: no diff.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map against all nine changed files. The provider's
Kafka model deliberately excludes `topics[].config`; no downstream
repository mirrors this nested field or its requiredness.

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

### Release note

```release-note
fix(kafka): allow topics to omit the optional Strimzi config map
```


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

## Summary by CodeRabbit

- **New Features**
- Kafka topic configuration is now optional. Topics can be created
without explicitly providing configuration settings.
- Topic configuration is omitted from serialized resources when not
specified.

- **Bug Fixes**
- Improved handling of topic resources without configuration, including
correct readiness validation and rendering.

- **Documentation**
- Updated Kafka configuration documentation and schemas to reflect that
topic configuration is optional.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 21:29:50 +04:00
scooby87 8540e746f8 fix(seaweedfs): parse naming-audit timestamps portably across GNU and BSD date (#4282)
Fixes #4273

`hack/seaweedfs-naming-audit.sh` read PV and `first_deployed` timestamps
with `date -u -d`, a spelling only GNU `date` accepts. On a host with
BSD `date` (macOS, or any host without GNU coreutils on `PATH`) the call
failed, its stderr was discarded by the `2>/dev/null` on the same line,
and `|| return 0` turned that host-tool failure into the audit's "no
epoch available" signal. Every tenant then read as MIXED with "direction
cannot be established", as though the cluster carried no evidence rather
than the host carrying no readable clock — and since the audit gates a
destructive runbook step, that indistinguishability is the problem.

A new `parse_epoch` helper tries the GNU spelling (`date -d`) and then
the BSD one (`date -j -f`), each with its own stderr muted, and uses
whichever the host speaks. Both call sites (`first_deployed` and
`pv_epoch`) route through it, so the audit now works on GNU and BSD
hosts alike with no behaviour change for a valid timestamp on a GNU
host.

Crucially it keeps the two outcomes the issue asks to be separated
distinct. A timestamp that is *present but readable by neither form* is
no longer folded into the silent absent-evidence branch: `parse_epoch`
fails loudly via `audit_fatal`, names the value, and aborts — the same
fail-closed contract the rest of the script already follows. The
legitimate absent-evidence cases the comment at the `pv_epoch` block
reasons about — an unbound claim, a NotFound PVC/PV — stay on their
benign path and still degrade to the conservative "classify by hand"
note.

## Tests

The suite is bats via `hack/cozytest.sh`. New coverage, each RED before
the fix and GREEN after (mutation-verified: reverting the fix turns them
RED again):

- `parse_epoch` reads an RFC3339 timestamp to the epoch the issue
quotes, on GNU or BSD date. The test runs under a deliberately non-UTC
`TZ` (`XXX5`), not UTC: both call sites pass `-u`, which makes GNU and
BSD alike parse the zoneless 19-char value as UTC, so the absolute epoch
is TZ-independent — measured on a BSD host, `1789470000` with `-u`
versus `1789488000` without. Dropping `-u` would move the value, so
running under a non-UTC `TZ` pins that flag as a regression guard. The
classifier's own relative comparison is timezone-immune regardless.
- `parse_epoch` fails loudly, on stderr, on a value neither form can
read — and writes nothing to stdout.
- The MIXED path classifies as OVERLAP under a BSD-only `date`,
byte-identical to the golden the GNU path produces, instead of
"direction cannot be established". A `date` shim on `PATH` reproduces
the bug deterministically on a GNU CI runner, where it is otherwise
invisible.
- The `first_deployed` call site (the second one the issue names) parses
under a BSD-only `date` instead of aborting the whole audit.
- A PV whose `creationTimestamp` is present but unparseable is fatal,
not the absent-evidence branch — proving the two are not collapsed at
the real call site.

`dash -n` confirms the script stays POSIX-clean for the CI shell.


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

- **Bug Fixes**
  - Improved timestamp handling across GNU and BSD environments.
- Audits now report an explicit failure when a provided deployment or
volume timestamp cannot be parsed, instead of treating it as missing
evidence.
  - Preserved the existing behavior for genuinely absent timestamps.

- **Tests**
- Added coverage for GNU and BSD timestamp parsing, invalid values,
deployment timestamps, and invalid volume timestamps.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 20:28:43 +03:00
Aleksei Sviridkin 939e6a118d fix(mongodb): fill the dashboard credentials on a first install (#4014)
## What this PR does

The dashboard Secret `<release>-credentials` ships with an empty
`password` and an empty `uri`, and keeps them for the whole life of the
database.

`templates/credentials.yaml` resolved the password with `lookup` from
`internal-<release>-users`, which the Percona operator creates only
after Helm has already rendered the release. The first-install lookup
therefore returns nothing, and no later event re-renders a release whose
chart and values are unchanged, so the empty values are never repaired.
The chart stated this in a comment and in the README, and two unit tests
asserted the empty values as intended behaviour.

The chart now owns those credentials. It generates the operator's
system-user passwords, writes them to the Secret named by
`spec.secrets.users`, and fills the dashboard Secret from the same
values in the same pass, so the two cannot disagree.

Two properties the implementation keeps:

- An upgrade never rotates a live password. Every value is reused from
the existing Secret when there is one, and generated only when neither
the users Secret nor the operator's internal copy holds it.
- A database created before this change keeps the Secret the operator
generated for it. Ownership is decided by the
`meta.helm.sh/release-name` annotation rather than by the Secret's
existence, so the chart neither tries to adopt an object Helm did not
apply, nor drops it from the manifest on a later upgrade and deletes it.
Such a release gets its `<release>-credentials` filled on its next
upgrade, with the password the operator had already assigned.

Verified on a running v1.6.1 install with the pre-change chart:
re-rendering the release filled `password` and `uri` from the operator's
existing Secret, the password was not rotated, the pod was not
restarted, and those credentials authenticate as `databaseAdmin`.

`helm unittest`: 137 tests, 10 suites, all passing. The suite now mocks
`lookup` through `kubernetesProvider` for the three branches a fresh
render never reaches: a helm-owned users Secret is reused, a Secret the
release does not own stays out of the manifest while its password still
reaches `<release>-credentials`, and the operator copy rebuilds a
missing users Secret. Forcing generation over a live Secret, dropping
the operator-copy fallback, or inverting the ownership test each turns
the suite red. On the not-owned path a missing password key now fails
the render instead of producing a password the users Secret never
receives, and the README upgrade section describes the carry-over rather
than a rotation.

### Screenshots

Not a UI change in this repository. The dashboard renders the Secret as
it finds it; only its contents change.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff. The diff touches three files, all under `packages/apps/mongodb`:
one template, its test suite, and the package README. No package is
added, renamed or removed; `values.yaml` and `values.schema.json` are
untouched, so no field, default or version enum moves; no output Secret
or Service is renamed, and `<release>-credentials` keeps its name;
nothing under `hack/`, `packages/core`, or the `ApplicationDefinition`
CRD is involved.

The README edit rides the path that is already automated: `mongodb` is
in the `APPS` list of the website `Makefile`, so the reference page is
regenerated from it on a stable tag.

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

### Release note

```release-note
fix(mongodb): the dashboard credentials Secret is now filled on a first install, instead of carrying an empty password and uri until someone runs helm upgrade by hand
```


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

* **New Features**
* MongoDB installations now provide usable credentials and connection
URIs from the first deployment.
* System-user passwords are generated automatically and stored securely.
  * Existing passwords are preserved during upgrades without rotation.
* Connection details support both replica-set and sharded MongoDB
deployments.
* Credentials include all required system users with generated
passwords.
* Credential values are restored when an existing users secret is
unavailable.

* **Documentation**
* Updated MongoDB credential documentation to describe automatic
generation, secure storage, and upgrade behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 19:36:17 +04:00
Alexey Artamonov 324a342e61 test(seaweedfs): make the BSD-date shim honor the audit's real format string
The two end-to-end tests that reproduce issue #4273 on a GNU runner drive
the audit through _write_fake_date, a `date` shim that rejects the GNU
`-d` spelling so the script must take its BSD `date -j -f` branch. But the
shim discarded the format argument the script passed and recomputed the
epoch with its own hardcoded `%Y-%m-%dT%H:%M:%S`. So the one string those
tests exist to protect -- the audit's BSD format spelling, which is the
whole of #4273 -- was never actually exercised: a regression in it left
the tests green. The only test that hits a real BSD `date -j -f` short-
circuits on the GNU branch on the CI runner, so the gap was invisible
exactly where the gate runs.

The shim now captures both the format and the value and uses the format
the audit passed. On a BSD host it honors it through real `-j -f`, so a
wrong format fails as production would; on a GNU-only runner, which has no
`-j` to enforce a format, it falls back to `-d` only for the exact format
the audit is supposed to pass, so a regressed spelling is not silently
rescued and the tests turn red on the very runner they guard.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-18 17:42:45 +03:00
Alexey Artamonov 6aa4ca954a fix(seaweedfs): parse naming-audit timestamps portably across GNU and BSD date
The naming audit read PV and first_deployed timestamps with `date -u -d`,
a spelling only GNU date accepts. On a host with BSD date the call failed,
its stderr was discarded by the `2>/dev/null` on the same line, and
`|| return 0` turned that host-tool failure into the audit's "no epoch"
signal -- so every tenant read as MIXED with "direction cannot be
established", as if the cluster lacked the evidence rather than the host
lacking a readable clock. Since the audit gates a destructive runbook
step, that indistinguishability is the defect.

parse_epoch now tries the GNU spelling then the BSD one (`date -j -f`) and
uses whichever the host speaks, so both call sites work on GNU and BSD
hosts alike with no behaviour change for a valid timestamp on GNU. A
timestamp that is present but readable by neither form is no longer folded
into the absent-evidence branch: it is fatal and names the value, keeping
a genuine parse failure loud and distinct from the legitimate "no bound PV
age" case (unbound claim, NotFound PVC/PV) the audit conservatively
degrades on.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-18 17:42:45 +03:00
Yan Bondarenko 1b576bc638 fix(registry): drop the backing object's UID from remapped error details
WrapPreservingStatus rewrites Group, Kind and Name of a backend status to
the aggregated resource but left Details.UID as the HelmRelease's, so a
client that keys on details saw an identity the tenant cannot look up.
The UID goes with the rest of the remap, and a test pins it.

Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Assisted-by: LLM
2026-09-18 17:30:33 +04:00
Yan Bondarenko 29351bd7fa fix(registry): report aggregated resource identity in errors
Backing resource names are not part of the aggregated API contract.
Clients need the requested resource identity and the original status
code and reason to handle rejected writes consistently.

Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Assisted-by: LLM
2026-09-18 17:30:33 +04:00
Yan Bondarenko 39da2300d2 fix(registry): keep backend status codes on aggregated write errors
Aggregated endpoints must retain backend HTTP codes and reasons so
clients can distinguish conflicts and admission failures. Dry-run
deletion also depends on current finalizers, which a cached read can miss.

Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Assisted-by: LLM
2026-09-18 17:30:33 +04:00
Yan Bondarenko a20c706940 fix(registry): align dry-run with storage semantics
Dry-run must expose the same deletion state as a real request without
persisting it. Shared option objects can hide dropped fields in tests,
and an unreachable patch method offers no API coverage.

Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Assisted-by: LLM
2026-09-18 17:30:33 +04:00
Yan Bondarenko a9812477e2 fix(registry): preserve write options in aggregated storage
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Assisted-by: LLM
2026-09-18 17:30:33 +04:00
Yan Bondarenko 9fac0add84 docs(mongodb): describe the upgrade as a carry-over, not a rotation
The upgrade section still promised a password rotation and a regenerated
credentials Secret. With the operator-copy fallback the chart reads the
current passwords from internal-<release>-users and writes the same values,
so nothing rotates and no session breaks.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:26 +04:00
Yan Bondarenko 0e17c6f65e test(mongodb): pin the reuse, fallback and ownership branches with lookup fixtures
Every test rendered with an empty lookup, so the branch that keeps a live
password could be deleted with the suite green. Three fixtures through
kubernetesProvider cover what a fresh render never reaches: a helm-owned
users Secret is reused, a Secret the release does not own stays out of the
manifest while its password still reaches the credentials, and the operator
copy rebuilds a missing users Secret. Values with URI-reserved characters
cover the userinfo encoding, and both Secrets are asserted Opaque again.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:26 +04:00
Yan Bondarenko f0da352b30 fix(mongodb): refuse to invent a password for a users Secret the chart does not own
On the not-owned path a missing password key produced a generated password
that reached <release>-credentials but never the users Secret the operator
reads, so the dashboard advertised a password MongoDB never had. The render
now fails and names the Secret and the key. The operator fills every key
itself, so only a hand-made partial Secret reaches this branch.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:26 +04:00
Yan Bondarenko 348a65d18b docs(mongodb): keep the credentials paragraphs on one line
Two paragraphs under Credentials were hard-wrapped at 80 columns, the one
place in this README that was; the rest follows the one-line-per-paragraph
rule the repository asks for in markdown.

Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Assisted-by: LLM
2026-09-18 17:30:26 +04:00
Yan Bondarenko 66a00f552e fix(mongodb): url-encode the username in the connection uri
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:26 +04:00
Yan Bondarenko b4be956d7e fix(mongodb): fill the dashboard credentials on a first install
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:26 +04:00
Yan Bondarenko 4dc6065ec9 fix(backups): give the sidecar sizing its reason per caller
The Keycloak comment and the helper doc claimed the tenant LimitRange as the
reason for the resources, but cozy-keycloak has no LimitRange and neither
does a tenant that leaves resourceQuotas empty: there the sidecar had no
requests and no limit at all, and the change gives it a 1Gi ceiling where
there was none. The comments and both doc blocks now say what applies to
each caller. No rendered object changes.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:20 +04:00
Yan Bondarenko 9214430bd8 fix(backups): give the barman-cloud sidecar its own resources
The barman-cloud plugin injects its sidecar without resource requests or
limits. Tenant namespaces ship a LimitRange that defaults containers to
128Mi, so the sidecar inherits that default and is OOMKilled mid-backup:
the ObjectStore stays healthy, the Cluster stays Ready, and only the
backup fails. A cgroup high-water mark of 254 MiB on a 38 MB database
shows the default cannot hold it.

The shared cozy-lib helper and the Go path that builds the platform's own
ObjectStore now request 100m/256Mi and cap memory at 1Gi. No CPU limit is
set, matching the entityOperator precedent in the kafka chart: a
throttled sidecar stalls WAL archiving instead of failing it.

The helper is renamed from checksumSidecarConfiguration to
sidecarConfiguration, since it no longer carries only the checksum pin.

Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:20 +04:00
Yan Bondarenko bc2ba106e1 docs(cluster-api): explain the omitted migration priority by the pinned API
CAPK v0.1.10 pins kubevirt.io/api v1.5.0, whose
VirtualMachineInstanceMigrationSpec carries only vmiName; upstream #392
sets priority against v1.9.0. The typed migration does not compile against
the pinned API, which is why the patch builds it unstructured.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:13 +04:00
Yan Bondarenko 0e4d6a305c test(cluster-api): declare the subshell EXIT traps in the CAPK bats suite
hack/bats-no-exit-trap.bats is an exact ratchet, and the new suite installs
three traps inside explicit subshells without declaring them, so
make unit-tests fails on the merge commit. The header now declares the
count and says why the three are not debt.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:13 +04:00
Yan Bondarenko c85f35070c fix(cluster-api): bound pending migration submission
A migration that KubeVirt never processes must not block host eviction indefinitely. Keep its submission deadline separate from active migration and fallback drain windows.

Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:13 +04:00
Yan Bondarenko bb3a2fa220 docs(cluster-api): clarify trusted backport image delivery
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:13 +04:00
Yan Bondarenko c8f4906e93 fix(cluster-api): backport live migration before host eviction
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:13 +04:00
Yan Bondarenko 8ec76a3614 fix(kafka): make topics[].config optional
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:06 +04:00
Yan Bondarenko cee9065589 fix(linstor): wait for temporary probe devices
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-09-18 17:30:00 +04:00
Aleksei Sviridkin 88a7ea0e3c fix(kubeovn): retry VM migration setup after scheduling (#4253)
## What this PR does

Kube-OVN can drop VM migration setup when a migration enters
`Scheduling` before the target Pod is visible or the source or target
node is known. The phase may remain unchanged afterward, leaving the
target Pod waiting for network setup.

Backport the final fix from kubeovn/kube-ovn#6834 to the pinned v1.15.10
image: return an error for the transient missing-node case so the
existing rate-limited queue retries it. Keep the same-node behavior
unchanged. That backport can be removed when updating to v1.15.14 or
later.

Add a separate retry guard for the target-Pod creation race. It returns
an error when the informer cache has no target Pod yet, instead of
forgetting the migration event. This guard is not part of #6834 and must
remain until an equivalent fix is verified in the selected upstream
version.

Also include the existing kubeovn image target in the root build list.
Without it, a kubeovn patch produces an empty PR build matrix and the
image is omitted from the regular build and release paths.

Validation:
- All six regression cases pass on Linux/amd64 using the real controller
workqueue. The new missing-target-Pod case fails without its guard and
passes after the Pod appears in the informer cache, without changing the
VMIM phase. The existing deferred-node, already-scheduled, and same-node
cases continue to pass.
- All 19 build-matrix tests pass; the full matrix includes kubeovn
exactly once.
- Both lint steps, pinned golangci-lint v2.10.1 and upstream `modernize
-test -skipgenerated ./...`, pass for the Linux/amd64 target using Go
1.26.4. On macOS, modernize is built for the host and run with the Linux
target environment, since `go tool` otherwise tries to execute a
cross-compiled Linux binary.
- All three packaged patches apply cleanly to pinned v1.15.10 with `git
apply --check --whitespace=error`; the resulting source matches the
tested tree. Formatting and source/build whitespace checks pass.

The regression test covers controller queue behavior; the standard
container E2E environment does not cover DRBD-backed live migration. The
regular image build applies the backport and stamps the resulting digest
before packaging.

### Screenshots

Not applicable; 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:

The change adds an existing image target to the build list; package
layout, make target interfaces, chart values, APIs, and published
image-reference formats stay the same. CCP discovers package image
targets dynamically and does not carry the root build list. The website
has no documented workaround for kubeovn/kube-ovn#6823 or #6834 to
remove.

### Release note

```release-note
fix(kubeovn): retry VM migration network setup when the target Pod or source/target scheduling information is not yet available, and include the Kube-OVN image in the regular build matrix.
```


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

## Summary by CodeRabbit

- **Bug Fixes**
- Improved VM migration handling when source or destination node
scheduling information is temporarily unavailable.
- Migration events are now retried after scheduling completes instead of
being dropped.
- Preserved existing behavior when migration source and destination
nodes are identical.

- **Tests**
- Added coverage for migration scheduling and retry scenarios, including
partially assigned and same-node cases.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 17:29:45 +04:00
Aleksei Sviridkin 08a2e76707 fix(opensearch): remove the data PVCs when the application is deleted (#4136)
## What this PR does

Fixes #4086.

The OpenSearch operator provisions its data volumes through a
StatefulSet, so the PVCs carry no ownerReference, and the `opensearch`
app chart has no cleanup hook. Removing the application therefore leaves
every data volume `Bound`, counting against the tenant's storage quota
with nothing left in the namespace that could reclaim it. `clickhouse`,
`kafka` and `harbor` remove their operator-owned PVCs in a post-delete
hook.

This adds a post-delete Job to `opensearch` that requests the deletion
of the claims labelled `opster.io/opensearch-cluster=<release>` with
`--ignore-not-found --wait=false` and exits non-zero when the request
fails, with its own ServiceAccount, a Role limited to
`persistentvolumeclaims` (`get`, `list`, `delete`), the
`clastix/kubectl` image pinned by digest through `cozy-lib.image`, a
non-root restricted securityContext, `activeDeadlineSeconds: 120` so a
kubectl that never returns cannot hold the uninstall, and
`before-hook-creation` alone on the Job so a failed run and its log
survive; the ServiceAccount, Role and RoleBinding keep `hook-succeeded`.

The delete does not wait for the claims to disappear, on purpose:
OpenSearch data pods stop slowly, and a claim they still hold would keep
a waiting hook running into helm-controller's 300s uninstall budget. The
claims go once their pods have stopped, which is what the log says. This
is the shape the monitoring, etcd and seaweedfs hooks use. Retention is
not offered as a knob, consistent with the other charts; an opt-out can
be added if the maintainers prefer one.

`helm unittest`: 64/64 in the chart, including
`tests/cleanup_pvc_test.yaml` (hook annotations and deadline, selector
and exit path, digest pin and registry prefix, securityContext, Role
rules, RoleBinding). `helm template` renders the Job, ServiceAccount,
Role and RoleBinding next to the existing objects.

### Downstream repositories

A new hook template and its tests only: no values, schema, README or
defaults 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(opensearch): reclaim the data PVCs of a removed application with a post-delete hook
```


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

## Summary by CodeRabbit

* **New Features**
* Automatically cleans up orphaned OpenSearch persistent volume claims
after a release is deleted.
* Cleanup waits for associated pods to stop and uses restricted
permissions and hardened security settings.
  * Supports optional container registries with pinned cleanup images.

* **Tests**
* Added coverage for cleanup behavior, hook configuration, security
settings, registry overrides, and required access permissions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 17:29:38 +04:00
Timofei Larkin 8f19910988 chore(platform)!: remove the HTTPCache application (#4200)
## What this PR does

Removes the HTTPCache application.

Telemetry says it is the least installed app we ship: 3 instances in
June, July and August 2026, 4 so far in September, out of 1162-1418
clusters reporting per month. That is not zero, so I am not going to
claim nobody uses it. The maintenance side is what does not hold up.

`make update` in this package is broken. All eight sed lines in the
`update` target patch `images/nginx/Dockerfile`, but the image lives in
`images/nginx-cache/Dockerfile`, so the target fails on the first line.
It is the only package under `packages/apps/` pointing at a Dockerfile
that does not exist. The pins show what that cost us:

```
nginx              1.25.3   -> 1.31.5
ngx_cache_purge    2.5.3    -> 3.0.2
nginx-module-vts   0.2.2    -> 0.2.7
ip2proxy-c         4.1.2    -> 4.3.0
IP2Location-C      8.6.1    -> 8.7.0
ip2location-nginx  8.6.0    -> 8.7.0
ip2proxy-nginx     8.1.1    -> 8.2.0
51Degrees          3.2.21.1 -> 3.2.21.1 (frozen upstream)
```

So we ship an old nginx with third party C modules compiled into it, and
nothing tests it: no chainsaw suite, no `helm unittest` target. I don't
think we should keep carrying that for 4 instances.

Removed: the chart in `packages/apps/http-cache`, its ResourceDefinition
in `packages/system/http-cache-rd`, the values type in
`api/apps/v1alpha1/httpcache`, the `cozystack.http-cache-application`
PackageSource, its line in the `naas` bundle, the `httpcaches` entry in
`cozy:tenant:admin:base`, the dashboard icon, and the entry in the root
`Makefile` build list.

`cozy:tenant:admin:base` enumerates the `apps.cozystack.io` resources a
tenant owner may write, so `httpcaches` comes out of it with the rest of
the application. A cluster that freezes gets the grant back from the
migration instead, which keeps the chart's list tracking the catalog and
leaves no entry behind for a later sweep to read as dead config.

### Existing installations

A cluster that still runs an HTTPCache keeps it, served from a frozen
source. A cluster that never created one keeps nothing.

Without a migration an upgrade would break both.
`templates/sources.yaml` is a bare glob with no keep annotation, so Helm
prunes the http-cache PackageSource; the ArtifactGenerator is owned by
it and goes too, and both ExternalArtifacts go with the generator. Those
are the `chartRef` targets of `hr/http-cache-rd` and of every tenant
`http-cache-<name>` release, so all of them would sit `Ready=False` for
good and `HelmReleaseNotReady` would fire on every cluster with the naas
bundle.

Migration 58 runs as a pre-upgrade hook, before that prune, and branches
on whether any instance exists. Its header comment carries the mechanism
in full; this is the summary.

If one does, it copies the packages `OCIRepository` and pins the copy to
the manifest digest that object resolves at hook time, which is the
release the instances are rendering from right then. `spec.ref.digest`
is used when the installer pinned one; a tag or semver ref goes through
`status.artifact.revision`, whose digest half is the manifest digest.
`status.artifact.digest` is the checksum of the downloaded tarball and
cannot stand in, and a source with neither aborts the migration rather
than publishing a source that points nowhere. Reading the digest live
rather than baking one into the script makes the freeze a no-op by
construction and asks no question about which release was the last to
ship the chart.

The copy takes the url, the pull secret, the interval and any
verification block from the live object, so an air-gapped or mirrored
registry keeps serving exactly what it already served. It is written
with no owner references and no Helm metadata, and with the
`platform.cozystack.io/no-delete` label the platform chart puts on the
original, because it becomes the only chart source those instances have.
The PackageSource is then repointed at the copy, annotated
`helm.sh/resource-policy: keep` and stripped of its release metadata, so
neither Helm nor a later release adopts or prunes it. Helm re-reads that
annotation off the live object when it prunes, which is what makes
disowning from inside a pre-upgrade hook work.

Names stay as they are, deliberately. The ApplicationDefinition inside
that artifact hardcodes its `chartRef`, so a renamed PackageSource would
leave every release pointing at an artifact nobody produces. Because the
names hold, `hr/http-cache-rd` and every tenant release keep the
`chartRef` they already have and nothing goes not ready. What changes is
where the chart comes from: a pinned digest that no later release moves.

The migration also applies `cozy:tenant:admin:http-cache-frozen`, a
ClusterRole labelled `rbac.cozystack.io/aggregate-to-tenant-admin`,
granting the verbs `cozy:tenant:admin:base` used to carry on
`httpcaches`. The hook applies it before the upgrade drops the rule from
the chart, so an owner's access never lapses and nothing changes for
them. Without it a frozen instance would be visible and undeletable by
the person who owns it: the wildcard over `apps.cozystack.io` lives in
`cozy:tenant:base`, which aggregates into `cozy:tenant` and not into
`cozy:tenant:admin`, `cozy:tenant:view:base` is read only, and no role
in the tenant chain carries a verb on `helm.toolkit.fluxcd.io`.

If no instance exists, the migration removes the platform side instead:
the Package first and waited on, because the operator reconciles off an
informer cache and re-creates the release it owns while that Package is
still there, then the release, its ApplicationDefinition and its Helm
storage. Nothing is pinned or granted on a cluster that never used the
application.

Every path is fail-closed. A failed fleet scan, a failed read of the
source OCIRepository, a source with no digest to freeze on, a failed
patch or a failed delete stops the migration before it stamps the
version.

The migration is numbered 58, because main already carries a 57 from the
SeaweedFS volume-size-limit change and its `targetVersion` is already
58. #4287 wants 58 as well, so whichever of the two lands second
renumbers and bumps `targetVersion` with it. main is merged in rather
than rebased, because the branch carries commits that are not mine to
rewrite; the one conflict was `migrations.targetVersion`, resolved as
59, one past this branch's migration number.

### The procedure, written down

`docs/deprecating-applications.md` is new, and is the point of doing
this one carefully. It covers how to choose between migrating instances
onto a successor and freezing them, the chain that makes a bare chart
deletion strand every surviving instance, how the freeze reads its
digest off the cluster's own source, what the migration owes a cluster
on each branch, and where the tenant RBAC grant goes. `AGENTS.md` points
at it so the next retirement starts there rather than rediscovering it.

### If you run HTTPCache

Nothing changes on upgrade. Your instance keeps running and stays
manageable through the Cozystack API and the dashboard, and `kubectl get
httpcaches` keeps working.

What changes is that it is frozen. The chart now comes from the packages
artifact your cluster was already running, pinned by digest, and gets no
further updates from us, including security updates to the nginx build.
There is no replacement in the catalog. Delete the HTTPCache object when
you no longer need it, the way you always did; the migration keeps the
tenant permission that lets you.

### Verification

`helm template` on `packages/core/platform` before and after, for
`isp-full`, `isp-full-generic` and `isp-hosted`. In all three the
differences are the `http-cache` Package and PackageSource going away
and `migrations.targetVersion` moving from 57 to 58, which on a cluster
below 58 also renders the migration hook Job and its RBAC.

The migration suite `hack/migration-58-http-cache-freeze.bats` covers
both branches, the order of every step and each fail-closed path.
Freezing on the tarball checksum instead of the manifest digest,
dropping the `no-delete` label, dropping the `managed-by` null, dropping
the tenant grant, returning from the Package delete without waiting,
freezing unconditionally and granting on a cluster with no instance each
turn the matching test red.
`packages/system/cozystack-basics/tests/clusterroles-tenant-admin-aggregation_test.yaml`
pins the aggregation label the migration's ClusterRole reaches tenant
admins through, so renaming it fails in the chart rather than silently
on the clusters that need the grant.

Green: `helm unittest` for `packages/core/platform` and
`packages/system/cozystack-basics`, `make migrations-target-check`,
`hack/cozystack-version-stamp.bats`, `go test
./internal/controller/cacert/...`, `go build ./...` in the
`api/apps/v1alpha1` module, the console `ServicesTab` and
`sidebar-icons` suites with `tsc --noEmit`, `make rd-presets-check`, and
the bats suites that enumerate packages and image refs.

### Screenshots

The catalog entry and its nginx sidebar icon are gone. Nothing new to
show.

### Downstream repositories

Two repos are affected. No follow-up is open yet, so no box is ticked.

`cozystack/website` carries `content/en/docs/*/networking/http-cache.md`
in eight doc versions and lists `http-cache` in the hardcoded
`NETWORKING` list in its Makefile. The `next` page and the Makefile
entry are what need to go, released versions stay as history.

`cozystack/terraform-provider-cozystack` has a `cozystack_httpcache`
resource and data source. The resource keeps working after this: it is
hand written against its own terraform-plugin-framework model and
imports nothing from here. What breaks, once that repo bumps its
`api/apps/v1alpha1` pin past this release, is
`internal/provider/httpcache_model_test.go`, the per kind schema guard,
which is the only file over there importing this module. The resource
and the guard should go together.

The other repos have no reference to the package.

- [ ] 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(http-cache)!: remove the HTTPCache application from the catalog. New instances can no longer be created from the catalog and the `HTTPCache` values type is gone from the `api/apps/v1alpha1` Go module. A platform migration keeps existing instances working: on a cluster that runs one, the application is frozen on the packages artifact that cluster was already rendering from, pinned by digest, and stays manageable through the Cozystack API and the dashboard, but receives no further updates, including security updates to the nginx build. On a cluster with no instance the application is removed outright. There is no replacement in the catalog; delete the HTTPCache object when you no longer need it.
```


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

* **Removed**
* Removed the HTTP Cache application and its deployment, configuration,
dashboard metadata, and packaging from the platform.
  * The HTTP Cache application is no longer included in the NaaS bundle.
* HTTP Cache resources and related API definitions are no longer
available.

* **Migration**
* Existing HTTP Cache installations are frozen during migration;
environments without instances are cleaned up.
  * Platform migration target advanced to version 58.

* **Documentation**
* Updated storage immutability documentation to reflect Valkey instead
of HTTP Cache.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 17:03:21 +04:00
IvanHunters 4844e1d99c fix(opensearch-operator): stop excluding a node whose drain cannot finish (#4323)
## What this PR does

The previous patch let a permanently yellow cluster past the restart
health gate. With three or more nodes `packages/apps/opensearch` renders
`drainDataNodes: true`, and the restart then reaches
`PreparePodForDelete`, which excludes the candidate node from allocation
before it can know whether the drain will finish.

On the cluster that made the gate wait it never finishes: a shard on
that node has nowhere else to go, which is the same condition holding
the cluster yellow. The exclusion is only withdrawn after a pod is
really deleted (`rollingRestart.go`, `upgrade.go`), so a data node stays
out of allocation for as long as the restart is stuck, shedding its
shards onto the rest of the cluster and buying nothing. Before the
yellow gate this path was unreachable with draining on, so nothing was
written at all.

`patches/skip-unusable-drain-exclusion.diff` asks the allocator before
excluding anything, and only while nothing is moving, so a drain making
progress costs nothing extra. A started shard answering
`can_move_to_other_node: no` means the drain cannot finish: no exclusion
is written, and one left by an earlier pass is withdrawn. A drain that
can still progress behaves exactly as before, and the two-data-node path
is untouched because it never waits for a full drain.

Checking after the exclusion instead would flap: the next reconcile ten
seconds later would apply it again.

The v2.8.0 base has no allocation-explain call of its own, so the patch
adds the client method and a response type carrying only the field the
decision reads.

The second commit makes the shipped tests real. The image build ran `go
build` alone, so a test file that stopped compiling, or stopped
asserting, still produced an image. `go test` over the two package trees
the patches touch now runs in the builder stage; both run without a
cluster.

Upstream as opensearch-project/opensearch-k8s-operator#1585, fixed there
by opensearch-project/opensearch-k8s-operator#1586. Both patches go away
with the first operator release carrying that change.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map against the diff: two files under
`packages/system/opensearch-operator/images/opensearch-operator/`. No
package README or values schema, no API or CRD, no node prerequisites in
`hack/`, no vendored chart, no make target.

- [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(opensearch-operator): a rolling restart or upgrade that cannot drain its next node no longer leaves that node excluded from shard allocation for as long as the restart is stuck
```


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

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved node draining during OpenSearch maintenance and restarts.
* The operator now checks whether shards can move before excluding a
node from allocation.
* If a shard cannot relocate, the existing exclusion is withdrawn and
draining pauses, helping prevent nodes from remaining unnecessarily
unavailable.
* Existing behavior is preserved for two-node clusters and while shard
relocation is already active.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 15:17:18 +03:00
Yan Bondarenko 54ac8ed135 fix(opensearch): keep the PVC cleanup hook from holding the uninstall
kubectl delete waits for the claims to be gone unless told otherwise,
and OpenSearch data pods stop slowly, so the post-delete hook could
outlive helm-controller's 300s uninstall budget on a claim that would
not release and leave the HelmRelease stuck uninstalling.

The delete now runs with --wait=false, the shape the monitoring, etcd
and seaweedfs hooks use: the deletion is requested and the claims go
once their pods have stopped, which the log now says instead of
"complete". The Job carries activeDeadlineSeconds 120 as the backstop
for a kubectl that never returns and keeps only before-hook-creation,
so a failed run and its log survive. The Role drops watch, which
nothing needs once the wait is gone.

Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Assisted-by: LLM
2026-09-18 16:09:19 +04:00