Commit Graph
979 Commits
Author SHA1 Message Date
Aleksei Sviridkin 02b7651b90 test(mariadb-rd): pin the rendered grant surface whole
The exclude checks read .spec.secrets.exclude[].resourceNames[]?, which
sees only name-shaped entries. An exclude entry selecting by label was
therefore invisible to every one of them, and adding one revokes the
tenant's connection string and its trust anchor at once: matchName
returns true for every name when resourceNames is nil, and exclude is
evaluated before include and wins. The test that existed to stop exactly
that edit let it through.

The empty-selector scan keyed on the literal token matchLabels, so an
empty matchExpressions and a bare {} passed it anywhere in the file,
including under services, which the comment claimed the same pass
covered. Both are legal: the selector type inlines metav1.LabelSelector.

One more comment stated the mechanism backwards — an exclude entry that
lost resourceNames makes the backstop total rather than voiding it — and
the check under it required only that some entry carry the key, so an
added entry without one went unnoticed.

Comparing spec.secrets and spec.services whole closes all of it. The
comparison is against the rendered chart rather than the source file,
because templates/cozyrd.yaml globs cozyrds/* and ships every match:
reading one file leaves a second one, dotfiles included, granting
whatever it likes. A document count fails closed on that.

The two per-name tests are kept ahead of the comparison, subsumed but
more legible, because the runner stops at the first failure. The
block-extraction helper is gone: the cozytest runner injects return 0
before a column-zero brace, so its status was discarded there.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:57 +03:00
Aleksei Sviridkin ce2cabb2d6 test(mariadb): verify the tenant-ca projection end to end
The unit suite pins what the chart renders — the sentinel, its source
name, its key — from the same assumptions the chart itself encodes, so
the two agree by construction and neither can catch the other being
wrong. Only a live mariadb-operator says whether <release>-ca-bundle is
the right name, and only a live apiserver says whether the lineage
webhook promotes the projection to something a tenant can read. Both
converged engines already carry this step; mariadb had none.

The fixture sets no tls.issuer, which is the case worth covering rather
than a gap: TLS is always served and the projection template is ungated,
so the operator reconciles the source here too.

A negative satisfied by an absent label is also satisfied by a webhook
that stamped nothing all run, so the credentials Secret leads as a
positive control. For the same reason the key-bearing names are checked
for private key material before their absence from the registry is
required, the count of names that reached that check is asserted
non-zero, and the read must answer NotFound specifically: an RBAC denial
or an unavailable aggregated apiserver also makes it fail, and reading
that as "safely absent" is how the check would stop discriminating
without ever going red.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:55 +03:00
Aleksei Sviridkin 14af86aac0 test(mariadb): pin the tenant-ca sentinel instead of the declared source
The file asserted that the ApplicationDefinition declares the CA source
itself -- sourceSecretName, sourceKey -- and that the operator's ca-bundle
is granted by name in secrets.include. The chart renders none of that any
more. The trust anchor now reaches the tenant as the key-free
"<release>.tenant-ca" projection: a TenantProjection sentinel in the chart
names the extraction source, and the ApplicationDefinition selects the
result on internal.cozystack.io/tenant-ca. Three assertions therefore
pinned fields the file no longer contains, and a fourth pinned a by-name
grant this design deliberately withholds.

Pin the grant surface instead, the way nats-rd and mongodb-rd do: compare
the whole value of secrets.include against the credentials Secret plus the
tenant-ca selector, so neither an added name nor an added label slips past
a per-dimension check, and assert that neither the credentials Secret nor
the projection is named in exclude, which is evaluated first and wins. The
source declaration stays pinned where it is rendered, by
packages/apps/mariadb/tests/tenant_projection_test.yaml, which compares
sourceSecretName against an exact key-free name -- a stronger guard than a
grep here could make, since this file never sees the chart.

Every assertion guarding a key-bearing Secret is kept.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:53 +03:00
Aleksei Sviridkin 69d5ce0644 test(mariadb): assert the CA error names its Secret, and correct the ledger
The backup CA case is called "checks the CA file exists and names it" but
only guarded the first half. Deleting the Secret name from the error left
the suite green, so the reason MYSQL_CA_SECRET is passed to the CronJob at
all — telling the operator which Secret to go look at, since the client
reports a missing CA without naming a file or its source — was unpinned.

The cozyrds suite header also counted its own coverage wrong. It claimed
two live tests and three placeholders; there are seven and four. The four
exclude guards are live and went unmentioned, and the case reading the
source key is inert like its neighbours but carried no placeholder marker.
That header is the file's honest-coverage statement, so an understated
ledger is the one error it cannot afford.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:52 +03:00
Aleksei Sviridkin 6cb1914ec0 test(mariadb): assert the guards abort, not just that they are spelled
Three assertions matched text that survived the mutation they exist to
catch.

The backup CA guard was pinned by three separate substrings: the two test
expressions and the message. Flipping its exit status to 0, or short
circuiting the condition with `false &&`, leaves all three intact and the
suite green — while the backup runs on with --ssl-ca pointing at a file
that is missing or empty, and in the exit-0 case reports success having
dumped nothing. Match the whole construct through its exit instead.

The exclude entries were checked by name without checking they still sit
under resourceNames. A nil resourceNames matches every name, so dropping
that key turns the backstop into a match-everything selector while each
name is still present in the block. Assert the key.

The leaf certificate pinned renewBefore but not duration. cert-manager
rejects a duration that does not exceed renewBefore, so pinning one of
the pair lets the other drift into a certificate that renders but cannot
be applied.

Each is verified by the mutation it was blind to: exit 1 to exit 0, a
short-circuited condition, a renamed resourceNames key, and an 8760h
duration shrunk to 1h all now fail the suite.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:51 +03:00
Aleksei Sviridkin 66d2c9b399 test(mariadb-rd): scope the exclude guards to the exclude block
The backstop guards grepped the whole file for each name. Both lists sit
at the same indentation, so an entry in `include` satisfied a guard that
exists to prove the name is in `exclude` — and moving a name between them
is the single most dangerous edit this file allows, since exclude is what
keeps a Secret away from the tenant.

Moving `-ca`, the operator's CA private key, from exclude to include left
all ten guards green. So did moving `-root`. Extract the exclude block and
match inside it; both mutations now fail on the name that moved, and the
suite is unchanged otherwise.

Also correct two descriptions the TLS work outdated. The cleanup hook's
Role comment still said the rule covered two operator-generated Secrets;
it now covers four, and two of them are issued by cert-manager, which the
block above already explains. The runtime message called them all
operator-generated for the same reason.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:51 +03:00
Aleksei Sviridkin 18a8bf296d test(mariadb): scope the CA-bundle guard and name the CR field it checks
The guard asserting that the trust anchor is surfaced to the tenant
grepped the whole file. The bundle is also named in a comment and in the
caCert declaration, so either of those could satisfy it while the
secrets.include entry it exists to protect was gone. Scope it to the
include block, the way the sibling key-bearing-Secret guard already does.
Verified by deleting the include entry with a matching string left
elsewhere in the file: the guard now fails, where before it passed.

The suite header described a comment as stating that spec.tls.issuer is
true. There is no such field on the MariaDB CR — issuer is a chart value,
and what the chart renders is spec.tls.enabled, which is what the rest of
that sentence already describes. Name the field the assertions actually
check.

Also drop the claim that an 11.4 client verifies by default. The default
that changed in 11.4 is the --ssl-verify-server-cert flag, not whether
verification happens: without --ssl-ca that client has no trust anchor,
and falls back to an anti-tampering check derived from the password hash
which establishes neither the chain nor the hostname, and which cannot be
derived at all for a passwordless user. Passing both flags is what gives
chain and hostname verification, on either client version. The documented
command was already correct; the sentence around it invited dropping the
flag that carries it.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:51 +03:00
Aleksei Sviridkin da2f5c33eb fix(mariadb-rd): restore the release variable in the CA source name
The CA source name was rewritten to a literal release prefix on the
premise that no release variable exists in the template context. That
premise came from the lineage webhook, which renders a different field:
resourceNames in the secrets selectors, where the context really is only
kind, name and namespace.

caCert.sourceSecretName is rendered by the CA-extraction controller
instead, and that one supplies a release variable explicitly. It also
parses with missingkey=error, so an undefined key would fail loudly
rather than resolve to an empty string — the silent collapse the rewrite
was meant to prevent could not have happened.

Restore `{{ .release }}-ca-bundle`. It is also the better spelling on its
own: the literal hardcodes an assumption about the release prefix and
would point at a Secret that does not exist if that prefix ever changed,
while the variable is the release name by construction. The reference
implementation for this field spells it the same way.

Drop the guard that forbade the variable. The controller's own tests pin
that spelling as correct, so the guard would have started failing the
canonical form the moment the field became live.

Keep the test pinning the source name, and say in it which renderer this
field belongs to, so the two contexts are not conflated again.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:50 +03:00
Aleksei Sviridkin 5a27ee4e88 fix(mariadb-rd): exclude the exporter config Secret from tenant promotion
The exclude backstop enumerated the internal credential Secrets but
omitted `-metrics-config`. The operator writes it for every instance —
`MetricsConfigSecretKeyRef` at the pinned 25.10.2 fixes the name as
`<name>-metrics-config` — and its `exporter.cnf` carries

    [client]
    user=...
    password=...

in plaintext. The exporter user is granted SELECT, PROCESS, REPLICATION
CLIENT, REPLICA MONITOR and SLAVE MONITOR on `*.*`, so that password
reads every table in every database.

It is not a hypothetical Secret: the chart sets `metrics.enabled: true`
unconditionally, so it exists on every ready instance. The include side
selects by label with no name constraint, and a nil `resourceNames`
matches everything, so the exclude list is what keeps it away from the
tenant. Exclude beats include, so naming it there is sufficient.

This is inside the enumerable part of the gap, not the part the comment
already disclaims: that carve-out covers Secrets a future operator adds
or creates mid-rotation, and this one exists today under a stable name.

Also complete a sentence in the suite that lost the words naming which
issuer it describes.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:49 +03:00
Aleksei Sviridkin f2bce8f57d fix(mariadb-rd): name the CA source Secret with a key the context defines
The CA source was spelled `{{ .release }}-ca-bundle`. The webhook builds
the only context these templates are rendered against, and it carries
kind, name and namespace — no release. The template resolves to nothing,
so the name would come out as `-ca-bundle` and match no Secret.

The exclude entry a few lines down already names the same Secret the way
that works, `mariadb-{{ .name }}-ca-bundle`: the release prefix is a
literal and the instance name is the templated part. Spell the source the
same. One Secret described two ways in one file meant one of them was
wrong, and it was this one.

The declaration is inert until the extraction controller lands, so this
changes nothing that runs today. That is also why it could sit here
unnoticed: the test pinned the broken spelling and passed on it, which
would have made the name wrong exactly when it first started being read.
Pin the working spelling instead, and add a guard that no undefined key
is templated into a Secret name.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:49 +03:00
Aleksei Sviridkin 81c44574d0 fix(mariadb): scope the Secret guards to the secrets section
The awk that was supposed to read secrets.include reset on any two-space
key and then re-entered on the next four-space include:, so it swept
services.include as well. It passes today only because no Service happens
to end in -tls — the exact case the comment promised it handled would
have misfired on a Service name. The exclude reader had the same shape.

Gate both on the secrets: section header instead of on indentation alone.
Verified in both directions: a Service named -tls no longer trips the
guard, and a Secret named -tls under secrets.include still does.

Also correct which CA the backup client verifies against: the bundle
carries the chart's CA too under the cert-manager issuer, which is what
makes the unconditional --ssl-ca right in both configurations.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:48 +03:00
Aleksei Sviridkin a55dc5ff19 feat(mariadb): name the dangerous Secrets in the ApplicationDefinition exclude
The include selector matches by label and carries no name constraint, so
any Secret of this instance that acquires the tenant-ca label is promoted
to the tenant with its data copied verbatim. The label on the source is
policy-guarded; the one on the destination is not. Exclude is evaluated
first and short-circuits, so naming the dangerous Secrets closes the part
of that gap we can enumerate.

Listed by group with the reason each is dangerous: the private keys from
both issuers, the internal database credentials, the backup S3 keys and
restic password, and the registry pull secret.

The guarantee is only as wide as the list. Matching is by exact name with
no globs, so a Secret added by a future operator version, or one created
transiently during a rotation, is not covered — the protection that holds
in general is still that only the extraction controller writes the label.
mariadb-operator has no rotation-snapshot Secret of the kind that makes
this worse elsewhere; its Secret names are fixed.

The existing guard against a key-bearing name is rescoped to
secrets.include while here, since such a name under exclude is the
backstop working rather than a leak.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:47 +03:00
Aleksei Sviridkin e164050cc1 docs(mariadb): state what the guards and the docs actually cover
Four claims of mine were broader than the thing they described.

The bats header said it pins the invariants deciding whether a private
key can reach a tenant. Three of its tests touch the declaration that the
API does not accept yet, so they pin an intended spelling and nothing
more; if the field lands under another name they stay green while the
feature does nothing. Say which tests are live and which are placeholders.

The certificate comment said the operator skips reconciliation entirely.
It skips issuance, which is why SANs go unvalidated, but it still reads
the Secret for the pod annotation hash and status.

The README mapped the client's verification default onto the version
enum, which selects the server image. The default belongs to the client
library, so an 11.4 client verifies against a 10.6 server.

The backup guard is described as catching a missing or unusable bundle.
On a fresh managed-TLS install the bundle can be present and non-empty
while holding only the operator's client CA, and the failure lands at
verification instead. Note the window, and note in the README that a new
instance stays down until cert-manager issues.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:46 +03:00
Aleksei Sviridkin f0b77de1e2 docs(mariadb): correct the empty-selector blast radius and order the teardown
Two claims of mine were wrong about mechanism.

The empty-selector warning said such a selector would project every Secret
in the namespace. The lineage webhook computes those labels only for
objects whose ownership resolves to the application, so the reach is the
instance's own Secrets. Still the thing to prevent — that set contains
-ca-tls and -tls — but the stated reason should match the code, since it
is what a reader checks the guard against.

The teardown step told the user to delete both Secrets with no ordering.
The server keypair stays mounted in the running pods until the operator
reissues from its own CA and rolls the StatefulSet, so following that
literally leaves the next pod unable to start. Wait for the rollout
first.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:45 +03:00
Aleksei Sviridkin 2be135a821 fix(mariadb): pin the verification default to the version it changed in
The note said the hostname check is off on 10.11 and on across 11.x. The
default actually changed in 11.4. Measured all four versions this chart
offers: 10.6 and 10.11 leave it off, 11.4 and 11.8 enable it. Name the
versions rather than the series, since the reader is choosing between
exactly those.

Scope the key-bearing-Secret guard to the secrets block as well. It
scanned the whole file, so a Service whose name ends in -tls would have
tripped it while a Secret entry with a trailing comment slipped past —
the wrong answer in both directions for the check that decides whether a
private key can reach a tenant.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:45 +03:00
Aleksei Sviridkin 3de6181da0 feat(mariadb): surface the operator CA bundle as a tenant resource
The dashboard Role already let the tenant read <release>-ca-bundle, but
the ApplicationDefinition did not list it, so it was never marked as a
tenant resource and never appeared through the API. The documentation
tells clients to fetch exactly that Secret, which worked with kubectl and
not in the interface the platform actually gives tenants.

Name it in secrets.include, the same way kafka names its CA certificates.
This works against the current API, unlike the label selector beside it,
which stays for the projection an unmerged controller will publish — so
the trust anchor is reachable now rather than only after that lands.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:42 +03:00
Aleksei Sviridkin 38671c3ff0 test(mariadb): catch a bare empty label selector, not just the inline one
The guard against an empty selector matched only "matchLabels: {}". A
bare "matchLabels:" with nothing nested under it is the same empty
selector — it compiles to match-everything and would project every
Secret in the namespace to the tenant — and it slipped through. Check
that a bare selector is followed by an indented label as well.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:41 +03:00
Aleksei Sviridkin 85c3ff1419 test(mariadb): require the tenant CA label inside the selector
The check accepted the label anywhere in the ApplicationDefinition, so a
stray occurrence would satisfy it while the selector that actually
exposes the projection stayed empty or wrong. Scope it to the
matchLabels block it has to appear in.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:39 +03:00
Aleksei Sviridkin 38e2c4fea4 test(mariadb): close a quoting hole in the ApplicationDefinition guard
The check rejecting key-bearing Secrets from the tenant include list
matched only unquoted list entries, while its sibling check was already
quote-agnostic. A quoted entry would have passed both the guard and the
comment claiming it could not.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:38 +03:00
Aleksei Sviridkin 18215895fd fix(mariadb): apply TLS enforcement regardless of who owns the CA
tls.required was only emitted when the chart managed the certificates,
so setting it on an instance using the operator's own CA was accepted by
the schema and then dropped on the way to the CR — no enforcement, no
error, no warning. For a setting whose entire purpose is refusing
unprotected connections, silently ignoring it is the worst available
outcome.

The coupling was artificial: in the operator API required sits beside
enabled with no dependency on either certificate reference, and
enforcement works against operator-issued certificates just as well.
Emit it in every configuration and let it mean what it says.

Also loosen the two negative guards in the ApplicationDefinition check,
which required a literal closing quote and exact brace spacing and so
would have missed an unquoted key-bearing source.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:36 +03:00
Aleksei Sviridkin 4339eb4631 test(mariadb): pin the ApplicationDefinition trust-anchor invariants
The ApplicationDefinition is where a mistake decides whether a private
key can reach a tenant, and it is the one file in this change the chart
test suite cannot reach: helm-unittest only covers rendered templates,
and the *-rd packages expose no test target.

Add a shell check in the style of the existing kafka one, which the
Makefile discovers automatically. It pins that the CA source is the
operator's key-free bundle rather than either key-bearing Secret, that
the tenant selector is the specific label rather than an empty selector
that would match every Secret in the namespace, and that no key-bearing
Secret is listed by name.

Type required as a pointer while here. As a plain bool it rendered a
default of false in the generated documentation while the description
promised true, and the zero value could not represent an explicit false
for consumers going through the generated struct — which is the value
the documented migration path depends on.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38:35 +03:00
Andrey Kolkov 86fe8e5a7e feat(valkey): add managed Valkey service (#3380)
## What this PR does

Adds a **managed Valkey** service to the catalog, alongside managed
Redis.

> **Stacked on #3406** (redis-operator migration to freshworks-oss).
This PR targets that branch and depends on it; review/merge #3406 first.

### Why

Redis relicensed at 8.x to a source-available family (AGPLv3 / SSPL /
RSALv2), and the managed Redis app currently ships `v8: 8.4.0` with no
BSD-licensed option ≤ 7.2. That is a licensing risk for CNCF Incubation.
[Valkey](https://valkey.io) is the BSD-3-Clause fork of Redis 7.2.4,
governed by the Linux Foundation, and a drop-in replacement for Redis.
This is the same move Argo CD made —
[`cncf/foundation#750`](https://github.com/cncf/foundation/issues/750)
was closed with "no need for exception" after they migrated to Valkey.

### How it works

Valkey runs on the same operator as managed Redis. #3406 migrates that
operator from the archived `spotahome/redis-operator` to the
actively-maintained **freshworks-oss/redis-operator** fork, which adds a
native `spec.engine: Valkey` switch. With it, the operator runs
`valkey-server` / `valkey-cli` natively (in the launch command and in
its liveness / readiness / shutdown probes), so the chart uses the
official `valkey/valkey` image directly — **no Redis-compatibility shim
image**.

The app mirrors managed Redis: same shape/values (`replicas`,
`resources`/`resourcesPreset`, `size`, `storageClass`, `external`,
`version`, `authEnabled`), Sentinel-based HA, metrics exporter,
`WorkloadMonitor`s, dashboard resource map, and `ApplicationDefinition`
(`kind: Valkey`).

Versions offered: `v8 → 8.1.8` (default) and `v7 → 7.2.13` (the
BSD-3-Clause Redis 7.2 line, most conservative drop-in).

### How it was verified

- `helm template` renders cleanly for `v8`, `v7`, and `external: true`;
the `RedisFailover` carries `spec.engine: Valkey` and `image:
valkey/valkey:<version>`. `helm lint` shows only the pre-existing
icon-URL / `cozy-lib` warnings that managed Redis emits too.
- `helm template` of `valkey-rd` renders a valid `ApplicationDefinition`
(`kind: Valkey`, `plural: valkeys`, `category: PaaS`).
- Generated artifacts (`values.schema.json`, `README.md`, `types.go`,
`zz_generated.deepcopy.go`, the ApplicationDefinition `openAPISchema`)
reproduce with the pinned `cozyvalues-gen` v1.6.0 and `controller-gen`
v0.16.4 — root `make generate` and per-app generate both produce no
drift; `go build ./valkey/...` passes.
- A live-cluster e2e was not run from this environment; the Chainsaw
suite `hack/e2e-chainsaw/valkey/` (mirroring the redis suite) is
included and `hack/select-e2e.sh` maps the `valkey-application` source
to it, so it runs in CI once the freshworks operator image is built.

### Notes

- The Valkey logo (`packages/apps/valkey/logos/valkey.svg`) is a
placeholder in Valkey's brand colors, pending the official brand asset.
- A Grafana `db/valkey` dashboard was intentionally left out of this
initial PoC; it can follow.

### Screenshots

N/A — no UI changes.

### Downstream repositories

This adds a new managed-app kind, which likely warrants follow-ups in
`cozystack/website` (user-facing docs for the new service) and possibly
`cozystack/terraform-provider-cozystack`. No follow-up PRs are opened
yet — leaving these for a maintainer to decide, per the template
guidance.

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

### Release note

```release-note
feat(valkey): add managed Valkey service — a BSD-3-Clause, Linux Foundation-governed alternative to managed Redis, running on the freshworks-oss redis-operator via native spec.engine: Valkey (versions 8.1.8 and 7.2.13)
```


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

* **New Features**
* Added managed Valkey service support with high availability and
configurable replicas.
* Supports Valkey versions 7 and 8, persistent storage, resource
presets, authentication, and optional external access.
  * Added monitoring metrics and dashboard integration.
  * Added platform package integration and tenant administrator access.
* **Documentation**
* Added Valkey configuration guidance and storage-class immutability
documentation.
* **Tests**
* Added end-to-end coverage for deployment readiness, storage, and
replicas.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-02 18:18:42 +04:00
myasnikovdaniil 67b4e23ca2 test(tests): cut the noise in unit and e2e job logs (#3973)
## What this PR does

Unit test job log is 238,794 lines and 31MB, and 233,444 of those are
`set -x` trace from `hack/cozytest.sh`. The one failing assertion sits
at line 238,573. Nothing in that stream is needed there, `run_one`
already tees raw output to `$log` and the fail handler dumps all of it
for whichever test fails, so dropping the live stream loses output of
passing tests only.

`COZYTEST_TRACE` defaults to 1, so e2e call sites in
`packages/core/testing/Makefile` keep the live stream. For a long e2e
suite that stream is the only progress signal, and the only record at
all when a step timeout or a dead runner kills the job before the fail
handler runs. `bats-unit-tests` sets 0. Running the runner by hand stays
verbose, that is the case where a human is actually watching.

Second thing, previous-instance log capture now orders by restart
recency. Container cap (`COZY_PREVLOG_MAX=12`) is spent in namespace
order and "unrelated" meant only "in another namespace", time was never
a key. So a container that restarted once during bring-up competed on
equal footing with one that crash-looped during the test, and won
whenever the pod list reached it first. Two measured e2e jobs spent 1034
and 1545 lines on `virt-api` and `cdi-apiserver`, both restarted once at
install time by `Unable to create certwatcher`, hours before any app
suite ran. On a live stand 178 containers carry a restart and their
timestamps span 24 days, so the cap binds hard enough for order to
decide everything.

This orders, it does not filter. Install-time restart is the explanation
when install is what broke, and the capture has no reference point for
"when the test began" that would make a cutoff honest. Namespace stays
primary key, because a stale restart in the namespace under test beats a
fresh one three namespaces away. So it fixes cap displacement, not raw
volume of a single run.

`lastState.terminated.finishedAt` joins the row. The guard on it is
three deep and the innermost one is load bearing, which I got wrong
first time round and review caught: `finishedAt` is a metav1.Time behind
`json:",omitempty"`, omitempty cannot omit a struct, and the zero value
marshals to `null`. The kubelet does synthesize a terminated state with
no time, ContainerStatusUnknown on a pod deleted mid-run, which also
bumps restartCount and so clears the restart filter. kubectl renders
that leaf as the literal `<no value>`, and `<` sorts above every digit,
so under the reverse sort that row landed FIRST and displaced the most
recent real restart. Exactly the inversion the ordering exists to
prevent, plus a header reading `previous instance ended <no value>`.
Fixed at both layers because they fail independently: the template
guards the leaf so the string cannot be produced, and the sort
normalises any field that is not shaped like a timestamp to empty so the
ordering holds even if some other producer emits one.

Worth naming how that slipped through. I probed the template against a
live cluster, saw no `<no value>`, and read it as the guard being
correct. It only meant no container on that cluster was in that state.

Reading the field also fixes a latent bug it would otherwise have
caused: `read -r ... restarts` folds a trailing field into the last
variable, so the dump header would print
`restarts=1|2026-08-22T09:06:00Z`. Now it names the time, which is the
first thing telling a reader whether the dump predates the test.

### What quiet mode keeps and what it drops

Two things are dropped, and the second one came out of review rather
than out of me.

First, xtrace and stdout of tests that pass. You cannot diff a passing
test's trace against the failing one inside the same job log, and a
warning printed by a test that passed is gone.

Second, a test that is KILLED rather than failed loses its trace as
well. The fail handler runs only after the pipeline returns, so a hang
reaped by the job timeout never dumps anything, and quiet mode has by
then dropped the live stream. Verbose mode never noticed, because the
trace was already printed. Measured: verbose leaves the line the test
blocked on, quiet leaves the `╭` and nothing else. So this one is a real
regression on the unit lane, not a pre-existing gap.

An INT/TERM trap is the obvious repair and it does not work. POSIX sh
defers a trap until the current foreground command returns, so
installing one stops the runner dying on the signal at all: a suite hung
on `sleep 30` took 30s to exit with the trap against 2s under the
default disposition. GitHub escalates INT, then TERM, then KILL, so the
deferred handler gets SIGKILLed before it ever runs, and the grace
period goes on waiting for the hang. Built it, measured it, reverted it,
and the measurement is in the comment now so the next reader does not
repeat it. Making it work needs the test pipeline moved into the
background with `wait`, which is the interruptible one, and that is
surgery on the path every suite in this repo takes for a partial trace
in a rare case.

What is left for that case is the escape hatch below, which for this
lane costs 48s and no cluster. The same argument is why e2e keeps the
stream on: re-running there is an hour and a live cluster, so the record
has to be written as it goes.

Everything else stays. The `╭`/`╰` pair per test with elapsed time, so
which tests ran, in what order and how long each took is all still
there. Full trace, stdout and stderr of the failing test, same bytes as
before, because `run_one` tees to `$log` and the fail handler dumps it,
and that path is untouched. Anything printed outside `run_one` as well,
file level code, `.bats` parse errors, and the `--- running <file> ---`
markers. A hang still shows up as `╭` with no matching `╰`. And `make
unit-tests` stops at the first failing test anyway, so there was never
more than one dump to read.

Measured on a suite that is actually red (`hack/ghcr-mirror_test.bats`
fails on my machine because it reads a live cluster): verbose 1472
lines, quiet 63 lines, and the post-failure dump is 23 lines and
byte-identical in both modes.

Getting the trace back, both paths checked:

```
COZYTEST_TRACE=1 make unit-tests     # env prefix, 29200 trace lines
make unit-tests COZYTEST_TRACE=1     # make cmdline, same
hack/cozytest.sh hack/foo.bats       # by hand, verbose by default
```

This lane needs no cluster and runs locally in 48s, so anyone who wants
the trace just reruns it. That is exactly why the same argument does not
extend to e2e, and why the default stays 1 there.

Nothing else consumed that stream. Unit lane stdout goes only to the job
log, `cozyreport.sh` folds in crust-gather snapshots and the trap writes
those to disk independently, and the unit lane produces none of them.

### Verification

Measured on this PR's own run: unit job went from 238,794 lines and 31MB
with 233,444 trace lines, to 5,720 lines and 580KB with 0 trace lines
and 1548 tests OK. Those two totals are not strictly like for like, the
old run was red and this one is green, so the honest comparator is
233,444 trace lines down to 0.

Quiet output is byte-for-byte the non-trace subset of verbose, and the
fail handler dump is unaffected (checked with a deliberately failing
suite). Verbose output unchanged against main, byte-identical sorted on
`hack/bats-no-exit-trap.bats`. Other suites are not self-stable run to
run (temp names, epochs, stdout/stderr interleaving) so that is the
ceiling on that check, not a finding.

Eight tests on the capture and seven on the switch. Every fix is
mutation proven: reverting the template leaf guard, removing the sort
key normalisation, dropping the read variable, dropping `-s`, silencing
an e2e recipe, removing the fail handler dump, flipping the default, and
making the switch a no-op are each caught. Three of those guards did not
bite when first written, which review also found: both stability
fixtures were pre-sorted ascending, which is what sort's last-resort
whole-line compare reproduces with no `-s` at all, and no test fed a
six-field row through the whole script, so deleting the read variable
left the suite green at 39/39. Whole unit lane, 83 files, 1503 passed
and 1 failed. The failure is `hack/ghcr-mirror_test.bats`, it reads a
live cluster and fails identically under main's own runner.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map file by file against the diff. Nothing under
`hack/` is moved or renamed, no make target changes what it does
(`bats-unit-tests` runs the same files to the same verdict, only
quieter), and `hack/package.mk`, `hack/common-envs.mk`,
`hack/update-crd.sh` and `hack/e2e-prepare-cluster.bats` are untouched,
which is what ccp, external-apps-example, talm and ansible-cozystack
couple to. No package `values.yaml`, `values.schema.json`, CRD,
namespace, label, annotation or metric name is involved.

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

### Release note

```release-note
test(tests): bats unit test trace is now opt-in via COZYTEST_TRACE and quiet by default in `make unit-tests`, and the e2e previous-instance log capture now picks the most recently restarted containers first
```


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

- **Improvements**
- Test runs can suppress live trace output for passing tests while
preserving full diagnostics for failures.
  - Trace visibility can be customized through the test configuration.
- Previous-instance logs are prioritized by the most recent container
termination within the preferred namespace.
  - Captured log headers now show when the previous instance ended.
  - Undated logs are retained and placed after dated entries.

- **Tests**
- Added coverage for trace controls, failure handling, and recent-log
ordering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-02 10:44:23 +05:00
Aleksei Sviridkin c94a1da5bb feat(mongodb): add TLS support via PSMDB operator-managed certificates (#2692)
## What this PR does

Enables operator-managed TLS for MongoDB and stops handing tenants any
Secret that carries private key material.

- Percona Server for MongoDB serves TLS unconditionally (`mode:
preferTLS`), so this PR drops the misleading `tls.enabled` toggle rather
than pretending the chart controls it.
- The tenant no longer receives the operator's CA Secret
(`<release>-ca-cert`, which holds `ca.key`) or the leaf
(`<release>-ssl`). Both were previously reachable, and either one is a
private key in tenant hands, the CA one is a full trust-chain
compromise.
- The chart declares a `TenantProjection` sentinel naming its CA source
Secret (`<release>-ssl`, the operator-managed leaf, which carries
`ca.crt`), and the ResourceDefinition selects the resulting key-free
`<release>.tenant-ca` projection by the engine-agnostic
`internal.cozystack.io/tenant-ca` label. The CA-extraction controller
reads the sentinel and publishes the projection holding only `ca.crt`.
The tenant gets what it needs to verify the server and nothing else.

### Why the include also carries an exclude list

The trust-anchor selector matches by label with no name constraint, so
any Secret in the namespace that acquired that label would be promoted
with its data copied verbatim. The enumerable dangerous set is therefore
excluded explicitly: the CA, both leaves, the rotation snapshots
(`-ssl-old`, `-ssl-internal-old`, same server keys as the leaves, and
unlike them they carry no owner reference, so an interrupted rotation
can leave them behind after the release is gone), the system-user
passwords, the at-rest encryption key, the keyfile, the object-storage
credentials, and both forms of the operator's internal Secret.

That backstop is bounded, and the comment says so: matching is by exact
name with no globs, so a Secret a future operator version introduces, or
a transient object an issuer creates mid-renewal, stays in scope of the
label-only include. The per-user Secrets cannot be enumerated at all,
since their names carry the username. The primary defence remains that
the controller stamps only its own projection; this closes the
enumerable set, not the whole surface.

### Release note

```release-note
feat(mongodb): stop exposing TLS private key material to tenants and publish the CA as a key-free trust anchor
```
2026-09-01 14:16:24 +04:00
Aleksei Sviridkin edd767ea0f feat(nats): publish ca.crt-only trust anchor to tenants (#3340)
## What this PR does

Converges nats onto the key-free CA trust-anchor contract. A TLS-enabled
nats release renders a `TenantProjection` sentinel naming the chart's
cert-manager CA Secret as its source, and the nats ResourceDefinition
selects the resulting `<release>.tenant-ca` object by label. The
projection is built by the CA extraction controller in
`internal/controller/cacert`, which parses the source certificate and
re-encodes it into a single `ca.crt` key, so a tenant that enables TLS
gets something to verify the server against without ever getting a
private key. No key-bearing Secret is granted to a tenant, by name or by
label.

`packages/apps/nats/templates/certmanager.yaml` is not touched. An
earlier draft of this change stamped publish labels onto the CA
`Certificate`'s `secretTemplate`; the controller that shipped consumes a
sentinel object instead, so the chart now declares its source rather
than marking it. The functional change is one new template and one
`matchLabels` entry in the ResourceDefinition.

The sentinel is gated on the chart's effective TLS state, which is
tri-state: left unset it follows `external`. That gate is the single
divergence from the postgres implementation of the same contract, and it
exists because the nats CA is only issued while TLS resolves true. An
ungated sentinel would sit `Ready=False/SourceNotFound` forever on every
plaintext release.

The CA extraction controller is already on main, so this is live on
merge rather than dormant until a follow-up lands. The
`do-not-merge/hold` label predates that and no longer guards anything.

## Verified against a running cluster

The premise this rests on is that cert-manager populates `ca.crt` on the
Secret of a `selfSigned`, `isCA: true` Certificate. Nothing in this
repository witnesses it: `hack/e2e-chainsaw/` has no nats suite, and
nats appears in no e2e workflow, so every unit test here encodes the
same assumption the chart encodes and the two cannot catch each other
being wrong. I closed it against a live cluster instead, running
cert-manager v1.20.2, the version this repo ships, on two independent
nats releases:

- `nats-<name>-ca`, the chart's CA Secret, holds `ca.crt`, `tls.crt` and
`tls.key`. The declared source key exists.
- `nats-<name>.tenant-ca`, the projection, holds exactly one key,
`ca.crt`.
- The projection's SHA-256 certificate fingerprint equals the source's,
so it carries that CA and not some other certificate.
- The projection carries `internal.cozystack.io/tenant-ca: "true"` and
`internal.cozystack.io/tenantresource: "true"`. The second is stamped by
the lineage webhook, computed from the ResourceDefinition selector this
PR adds, so its presence means the whole chain held.
- `kubectl get tenantsecret nats-<name>-ca` returns NotFound. The
key-bearing Secret is not served through the tenant API.

What is still missing is an automated regression witness for that
premise. Adding one means a new chainsaw suite for nats, which has no
e2e coverage of any kind today, and that is a larger change than this
one.

## What the tests pin

61 helm-unittest cases and 3 bats cases. The sentinel's absence of
`ownerReferences` (an owner would short-circuit the lineage walk before
it reaches the Flux label the attribution depends on), the length of
`spec.projections` as well as its single entry (the controller refuses a
multi-entry sentinel, so a second entry is a silent loss of the anchor
rather than a leak), both implied legs of the tri-state TLS gate, and
`notExists` on either visibility label for both key-bearing
Certificates.

The bats guard covers the two grant surfaces nothing else asserts on.
The ResourceDefinition is rendered, by the `nats-rd` chart inlining
`cozyrds/*`, but that chart has neither a tests directory nor the `test`
target `hack/helm-unit-tests.sh` looks for, and no
`packages/system/*-rd` package does. The dashboard Role is rendered by a
chart that has both, and no suite under `packages/apps/nats/tests/`
asserts on it.

Both are compared whole rather than filtered down to the entries that
mention a Secret, because every such filter admits the spelling it did
not anticipate. A name whitelist passes an added label selector, and
`controller.cert-manager.io/fao` is stamped on both key-bearing Secrets,
so a selector on it is a one-line grant of the CA private key. On the
Role side, a rule with no `resourceNames` names no Secret, a rule with
`resources: ["*"]` names no resource, and a ClusterRole is not a Role,
while all three grant read access to the CA key. Comparing the rendered
output whole needs no filter and leaves no spelling to find.

## Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff, file by file. No trigger fires, and no follow-up PR is owed.

`packages/apps/nats/templates/tenant-projection.yaml` is a new template
and `packages/apps/nats/tests/` holds chart tests. No package is added,
renamed or removed; `values.yaml` and `values.schema.json` are
untouched, so neither the hand-written Terraform schema, its version
enums, nor its restated defaults have anything to track. `kind`,
`plural` and `release.prefix` are unchanged, and no output Secret or
Service is renamed, so the provider's concatenated name lookups still
resolve.

`packages/system/nats-rd/cozyrds/nats.yaml` is edited in place rather
than renamed, so the ccp external-app skill's dependency-contract anchor
still resolves. The provider offers no typed access to the new
projection, which is the position it already holds for every Secret it
does not model, and not a break.

`packages/apps/nats/README.md` is covered by the one automated path that
does exist: `nats` is present in the website `Makefile` `APPS` list, so
the docs bot regenerates the managed-apps reference page from this
`README.md` on the next stable tag. This is the silent-skip failure mode
the trigger map warns about, so I checked the list rather than assuming;
an app missing from it gets no page at all, which is what happened to
`opensearch`.

`hack/check-nats-rd-secrets.bats` is a new file, not a move or a rename,
and it changes no make target's contract: `Makefile` globs `hack/*.bats`
into `BATS_UNIT_FILES` automatically. The ccp skills anchor on
`hack/package.mk` and `hack/common-envs.mk`, neither of which is
touched.

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

Part of #2814.

### Release note

```release-note
feat(nats): expose the CA certificate to tenants as a key-free `<release>.tenant-ca` Secret
```
2026-09-01 13:11:22 +04:00
Aleksei Sviridkin 65e700486b feat(qdrant): publish ca.crt-only trust anchor to tenants (#3341)
## What this PR does

Converges qdrant onto the key-free CA trust-anchor contract (#2814), the
same way #3340 does it for nats. The chart declares a `TenantProjection`
sentinel naming its cert-manager CA Secret. The CA-extraction controller
reads that declaration and publishes a key-free copy, `ca.crt` and
nothing else, as `<release>.tenant-ca`. The qdrant ResourceDefinition
selects that projection by label so it shows up in the tenant's secret
list. No key-bearing Secret gains a tenant-visible label.

The sentinel is gated on the chart's TLS condition. Engines whose
operator mints a CA unconditionally do not need that, but qdrant only
has a CA while TLS is on, and an ungated sentinel would sit
`Ready=False`/`SourceNotFound` on every plaintext release.

This was held while the CA-extraction controller was still open. That
controller landed in #3407, so the sentinel is honoured as soon as this
merges and the hold is gone.

Tests pin the invariants that otherwise fail silently. The sentinel
carries no `ownerReferences`, so the lineage walk falls through to the
Flux release label. Neither key-bearing Certificate stamps a
tenant-visible label on its Secret. The two objects that decide which
Secrets a tenant may actually read, the dashboard Role with its binding
and the ResourceDefinition, are pinned whole rather than checked for
known-bad names: a rule granting secrets with no `resourceNames` is
unrestricted rather than narrow, and a label selector can reach a
key-bearing Secret while every name in the file stays correct, so
neither adds a name for a probe to notice.

Part of #2814.

### Follow-ups

What this PR does not do, and where each belongs instead.

- nats converges the same contract in #3340 and has no
`dashboard-resourcemap_test.yaml`, so the dashboard Role pin belongs
there too. Tracked in #3701 along with postgres, which already carries
the label selector with no guard behind it.
- The qdrant e2e fixture runs plaintext
(`hack/e2e-chainsaw/qdrant/qdrant.yaml` sets `external: false`), so
nothing in CI walks the path from the sentinel to `tenantsecrets`.
postgres has a `verify-tenant-ca-projection` step to copy.
- `hack/check-qdrant-rd-secrets.bats` is engine-agnostic apart from two
literals, the chart path and the expected Secret set. The third copy of
it is the point to parameterise over `packages/system/*-rd/cozyrds/`
instead of copying again.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff, file by file. Nothing needs a manual follow-up, but not for the
reason a values-only walk would give: the website reference page for an
app is regenerated from that package's `README.md`, and this PR does
change it. `qdrant` is in the app list hardcoded in the website
`Makefile`, so the docs bot picks the new section up on the next stable
tag and there is nothing to open by hand. The Terraform provider's
schema is hand-written against `values.schema.json`, which is untouched,
and the provider offers no typed access to the new projection, the same
position it holds for every other Secret it does not model. No package
is added, renamed or removed, and the `cozyrds` file is edited in place
rather than renamed, so the ccp dependency-contract anchor still
resolves.

User-facing documentation for consuming the trust anchor ships with the
chart README, matching #3340.

- [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
feat(qdrant): expose the CA certificate to tenants as a key-free `<release>.tenant-ca` Secret
```


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

* **New Features**
* Tenant environments can securely access the Qdrant TLS CA certificate
without receiving the private key.
* TLS CA access is automatically enabled when TLS or external access is
configured.

* **Documentation**
* Expanded TLS guidance explains certificate validation and provides
commands for retrieving the tenant CA certificate.

* **Tests**
* Added coverage for TLS projections, secret visibility, dashboard
resources, and secure fail-closed secret exposure.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-01 13:10:37 +04:00
Aleksei Sviridkin 8b6867dc47 fix(mongodb): correct the trust-anchor prose and two e2e assertions
The name-clash note claimed the shared TLS helper documents
<release>-ca-cert as the conventional key-free anchor. It documents the
opposite: the canonical name is <release>.tenant-ca, and it names this
engine as the reason, because MongoDB already claims <release>-ca-cert
with a key-bearing Secret. There is no clash to warn about and the chart
follows the convention rather than deviating from it. The paragraph
above already says <release>-ca-cert holds the CA private key, so drop
the note instead of inverting it.

Guard the ownerReference assertions against an absent list. length() of
null is a type error rather than a failed comparison, and the harness
returns it as an error, which aborts the poll instead of retrying — so
the timeout never applies while the operator is still patching the
reference on.

Restore the two-variable form of the fingerprint comparison. Inlining
both command substitutions into the test swallows a non-zero exit: with
no openssl on PATH both sides are empty, they compare equal, and the
check passes having compared nothing.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03:00
Aleksei Sviridkin 01cfb5fda0 fix(mongodb): correct what a missing extraction controller does
The README said the sentinel would sit inert with nothing else
affected. The CRD is a template of the controller's own chart, so a
cluster without the controller has no TenantProjection kind and the
release fails to render at all. The caveat also guarded a state that
cannot occur, since the controller is a mandatory platform component —
drop it rather than restate it, matching the postgres chart, and keep
the paragraph explaining the choice of source Secret.

Enumerate all three key-bearing Secrets in the registry negative. Two of
the three were listed, which leaves the third free to acquire the label
and leak a key, the exact reasoning the step above it already spells
out.

Collect TenantProjection status on failure. The controller reports
SourceNotFound and its siblings there, and without it a failed
projection surfaces only as an absent Secret with no reason given.

Rename the source-shape step, which no longer looks at the CA Secret,
and cross-reference the two halves of the leaf Secret's name.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03:00
Aleksei Sviridkin c0b2ddcdf7 test(mongodb): assert the published trust anchor end-to-end
The existing steps only assert that key-bearing Secrets are NOT promoted
to the tenant, and every one of those is satisfied by an absent label —
they hold just as well if nothing was ever published. Add the positive:
the projection exists, carries exactly ca.crt, matches the source
certificate by fingerprint, and is reachable through the tenantsecrets
registry rather than only as a raw Secret.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03:00
Aleksei Sviridkin 29d2955241 fix(mongodb): declare the trust anchor with a TenantProjection
A tenant verifying a MongoDB endpoint needs the CA certificate, and
every Secret that holds one also holds a private key: the operator's
<release>-ca-cert carries the CA key, the leaf <release>-ssl the server
key. Granting either would hand over the key along with the anchor.

Render a TenantProjection naming the source, so the CA-extraction
controller can publish the key-free <release>.tenant-ca, and select that
projection by label from the managed-service definition. The sentinel
carries no ownerReferences: it is resolved back to its release through
the label Flux stamps, and an owner reference short-circuits that walk
and silently costs the tenant its anchor.

The sentinel is ungated. The operator issues the certificate chain
whatever the chart asks for, so the source exists for every release and
there is no plaintext leg to skip.

The source is <release>-ssl rather than <release>-ca-cert. It is the
name this chart itself sets in spec.secrets.ssl, so the reference cannot
drift from the CR; the operator writes it on both its cert-manager path
and the self-signed fallback it uses when cert-manager is absent, where
no <release>-ca-cert exists at all; and it holds the merged old and new
CA through a rotation, where <release>-ca-cert holds only the new one
and would stop verifying pods still serving the previous leaf.

Withhold the key-bearing Secrets from the tenant explicitly as well. The
label selector matches on no name, so a single mis-stamp would promote a
private key; the exclude list is evaluated first and closes the
enumerable set.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:32 +03:00
Aleksei Sviridkin ea7bc00016 docs(mongodb): correct the TLS secret ownership and external SAN claims
The uninstall section promised that no manual cleanup of the TLS Secrets is
needed, reversing the instruction it replaced. That promise rests on an
ownerReference the operator patches on at reconcile time rather than sets at
creation, and two paths skip the patch: a dry-run reconcile returns before it,
and a Secret already present under the same name with a mismatched
cert-manager.io/certificate-name annotation is counted toward success and
returned on unpatched. The deletion finalizer does not cover the gap — it
enumerates only the users, internal-<release>, internal-<release>-users and
encryption-key Secrets. So a Secret predating the release at one of these names
can stay unowned and survive uninstall, and for <release>-ca-cert that orphans
the CA private key.

State the reconcile-time mechanism and tell operators to confirm the CA Secret
is gone. Pin the outcome with a chainsaw step asserting the CR owns all three
TLS Secrets on a normal install, so the claim goes red if the operator stops
patching instead of rotting as prose.

The external-hostname note described a permanently empty _namespace.host as a
bootstrap window that self-heals. A tenant with no host of its own and no
ancestor supplying one resolves to an empty host for good, leaving external
TLS hostname verification silently unavailable. Name that case alongside the
transient one, and say why the render is not failed closed: it would break
existing hostless tenants already running external access over IP.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 27f71bfbe9 fix(mongodb): deliver the TLS trust anchor without private key material
The tenant-facing paths handed over both Secrets the PSMDB operator creates
for TLS. <release>-ca-cert holds the CA private key, so read access is a full
trust-chain compromise; <release>-ssl holds the server private key, so read
access allows impersonation. A client verifying the endpoint needs ca.crt and
nothing else, so neither object belongs on a tenant-facing path.

Declare the trust anchor by name instead. The operator creates its CA Secret
itself and offers no way to label it, so the ApplicationDefinition names it and
the CA-extraction controller lifts ca.crt alone into the key-free projection.
The include selector matches that projection by label, and the dashboard Role
drops both key-bearing names without gaining the projection by name: a name
grant conveys whatever occupies the name, while the label path conveys only
what the platform vouched for.

Witness both delivery paths against a running cluster, since the two that
matter are separate. The Role is asserted as an exact grant with bounded rule
counts, because a rule with no resourceNames — or one granting every resource
— would otherwise pass an absence check while conveying every Secret in the
namespace. The label path is asserted as a negative on all three key-bearing
Secrets. A bats check pins the declaration's names and keys, which nothing else
covers: the field is pruned at admission until the controller ships, so drift
there would surface as an empty trust anchor with nothing red.

Drop the mongodb-rd unit test. That package has no test target, so the file
never ran, and what it asserted was the leak.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:41 +03:00
Andrei Kvapil df1345f56d test(valkey): port e2e to chainsaw suite
The app e2e harness on main is chainsaw (hack/e2e-chainsaw/<app>/), not the
older bats layout. Add hack/e2e-chainsaw/valkey/ mirroring the redis suite so
select-e2e.sh maps the valkey-application source to the valkey suite, and drop
the misplaced hack/e2e-apps/valkey.bats.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-09-01 11:44:07 +04:00
Andrei Kvapil 440c3b58f3 test(valkey): add e2e test and wire build, labeler and docs
Add the Valkey e2e bats test (mirrors redis), build the compat image in the
top-level build target, map the valkey scope to area/database in the PR
labeler, and list valkey among charts with an immutable storageClass.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-09-01 11:44:07 +04:00
Andrey Kolkov b9a95a0c4a fix(postgres): let in-place restore re-bootstrap instead of wedging (#3959)
## What this PR does

An in-place `RestoreJob` (one whose `targetApplicationRef` is omitted,
so the app is restored back into itself) re-bootstraps the Postgres app
under its own name. On stock v1.6.2 it never comes back up — the cluster
is deleted and then hangs in `Setting up primary` with
`*-full-recovery-*` pods crash-looping, or the HelmRelease upgrade fails
outright. There are two independent causes, both in the same restore
patch, and both surface only in place because a to-copy restore targets
a different app.

**1. Archive-vs-recovery `serverName` collision.** The driver set the
restored cluster's WAL-archive `serverName` equal to the recovery source
(`bootstrap.serverName = bootstrap.oldName = sourceServerName`), while
the chart pinned the archiver `serverName` to the release name. In place
those coincide, so the cluster archived onto the source's own WAL prefix
and `barman-cloud-check-wal-archive` refused to start with `Expected
empty archive`. Fixed by giving the restored cluster a fresh
`bootstrap.newServerName`, keyed by the `RestoreJob` UID (unique per
restore, stable across reconciles); the chart archives there while
`externalClusters` still recovers from `bootstrap.serverName`. This is
what manual `CNPG` strategies hit (their `serverName` defaults to the
cluster name, including the shipped
`examples/backups/postgres/10-cnpg-strategy.yaml`); the `cozy-default`
flow dodged it because its `serverName` is `<namespace>-<app>`.

**2. `useSystemBucket` guard.** The restore populates explicit S3
coordinates (`destinationPath` / `s3CredentialsSecret` / `endpointCA`)
but left `backup.useSystemBucket=true`, so the chart's
`bootstrap.enabled is incompatible with useSystemBucket` guard failed
the HelmRelease upgrade and the cluster never re-rendered. This is what
the default `cozy-default` (system-bucket) flow hits on an in-place
restore. Fixed by clearing `useSystemBucket` in the restore patch, since
the app is now on the explicit-creds path. Verified live on a v1.6.2
cluster: with `useSystemBucket` cleared, the same in-place restore
re-bootstraps to a healthy cluster and the sentinel row survives.

Both are exercised only by an in-place restore; the postgres backup e2e
drove only to-copy, so neither surfaced in CI. A step 50 in-place
restore now guards cause 1 (manual-strategy flow); cause 2 is covered by
a Go unit assertion (a system-bucket in-place e2e needs the platform
bucket wired in CI and is left as a follow-up).

This is a bug in released v1.6.2 (chart and driver are byte-identical on
`main`), so it is a backport candidate.

### Screenshots

Not applicable (no UI changes).

### Downstream repositories

The only externally visible schema surface is one new field,
`spec.bootstrap.newServerName`, in the postgres `values.schema.json` /
CRD; it is set by the CNPG backup driver on restore and never authored
by tenants, exactly like the existing `bootstrap.serverName` /
`bootstrap.oldName`. (The `useSystemBucket` clearing is internal to the
Go driver — `backup.useSystemBucket` already exists in the schema.) The
chart `README.md` is regenerated from `values.yaml` automatically by the
website docs bot on release, and `terraform-provider-cozystack` models
user-authored spec (its guard tracks top-level fields, not driver-set
nested ones), so neither needs a change in practice. Leaving every box
unticked per the trigger-map guidance so a maintainer confirms rather
than acting on a speculative follow-up.

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

### Release note

```release-note
fix(postgres): in-place restore no longer wedges — the restored cluster archives WAL under a fresh serverName distinct from the recovery source, and a system-bucket app is switched to explicit credentials so the bootstrap/useSystemBucket guard no longer fails its HelmRelease.
```


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

- **New Features**
- Added support for specifying a distinct WAL archive destination when
restoring PostgreSQL clusters.
- In-place restores now automatically use a fresh archive location while
continuing to recover from the original source.
- Added configuration and schema support for the optional
`bootstrap.newServerName` setting.

- **Bug Fixes**
- Prevented archive conflicts that could cause in-place restores to
fail.

- **Documentation**
  - Documented the new restore archive configuration and behavior.

- **Tests**
- Added coverage for unique restore destinations and in-place restore
validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-28 15:08:35 +04:00
Myasnikov Daniil c88da18139 test(tests): keep trace switch suite ASCII-only
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-28 15:31:22 +05:00
Myasnikov Daniil e949f39800 fix(e2e): preserve previous-log cap slots
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-28 15:29:29 +05:00
Myasnikov Daniil 76d397523b docs(tests): record why an INT/TERM trap cannot cover a killed test
The comment added with the trace switch pointed at a signal trap as the
fix for the one case quiet mode does not cover: a test killed rather
than failed never reaches the fail handler, so its trace is gone rather
than already printed. That advice is wrong, and wrong in a way that
costs whoever follows it.

POSIX sh defers a trap until the current foreground command returns, so
installing an INT/TERM handler stops the runner dying on the signal at
all. Measured: a suite hung on `sleep 30` exits in 30s with the trap
installed against 2s under the default disposition. GitHub's
cancellation escalates INT, TERM, then KILL, so the deferred handler
gets SIGKILLed before it runs -- no dump, and the grace period spent
waiting for a hang. Built it, measured it, reverted it.

Making it work means running the test pipeline in the background and
waiting on it, because `wait` is the interruptible one. That is surgery
on the path every suite in this repo takes, unit and e2e, to recover a
partial trace in a rare case that already has a 48-second escape hatch.
Not taken; recorded so the next reader does not spend the afternoon
finding out the same way.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 22:30:18 +05:00
Myasnikov Daniil a5c90d984e fix(tests): guard the finishedAt leaf and make three guards actually bite
Findings from two independent adversarial reviews of the preceding two
commits. Each was reproduced locally before being fixed.

The ordering inverted itself on a shape I had not probed. `finishedAt`
is a metav1.Time behind `json:",omitempty"`, which cannot omit a
struct, and its zero value marshals to `null`; the kubelet does
synthesize a terminated state with no time (ContainerStatusUnknown on a
pod deleted mid-run, which also bumps restartCount and so clears the
restart filter). Two guards deep left the leaf exposed, and kubectl
renders it as the literal `<no value>` -- which sorts ABOVE every digit,
so that row took the front of the queue from the most recent real
restart, and the header printed `previous instance ended <no value>`, a
claim about a time nothing observed. Verified with a text/template probe
over seven lastState shapes: `null` and a missing key both produced it.

Fixed at both layers, because they fail independently. The template now
guards the leaf, so the string cannot be produced. And the sort
normalises any field-6 value not shaped like a timestamp to empty, so
the ordering stays correct and the header stays quiet even if some other
producer emits one. Earlier verification against a live cluster showed
no `<no value>` and I read that as the guard being right; it only meant
no container on that cluster was in that state.

Three guards did not bite:

  - both stability fixtures were pre-sorted ascending, which is exactly
    what sort's last-resort whole-line compare reproduces with no -s at
    all, so the tests written for -s passed without it. Reversed; they
    now fail when -s is dropped.
  - no test fed a six-field row through the whole script, so the
    timestamp reached the dump header uncovered -- deleting the read
    variable left the suite green at 39/39. One integration test now
    asserts the header names the time.
  - the e2e guard matched any mention of COZYTEST_TRACE, so documenting
    the design over there would have turned it red. It now matches an
    assignment, and tolerating a comment is verified too.

Also: a failed sort no longer discards a whole namespace group. The
caller compares row counts to report the cap, so losing a group silently
became "0 of N captured, N more not captured" and sent the reader after
a cap that never fired. It falls back to discovery order and says so.
LC_ALL=C on the sort, since the key is fixed-format ASCII.

And the claim itself was too strong. "Nothing diagnostic is lost" holds
only for a test that RETURNS: the fail handler runs after the pipeline,
and there is no INT/TERM trap, so a test killed rather than failed -- a
hang reaped by the job timeout -- never dumps its log, and under quiet
mode its trace is gone rather than already printed. The comments in
hack/cozytest.sh and the Makefile now say so and name the escape hatch.
A trap that flushed the tail of the log would close it; left out
deliberately rather than bundled in here.

Every fix is mutation-proven: reverting the leaf guard, removing the
key normalisation, dropping the read variable, dropping -s, and
silencing an e2e recipe are each caught. Unit lane 1513 passed, 1 failed
(hack/ghcr-mirror_test.bats, which reads a live cluster and fails the
same way under main's runner).

Assisted-By: Claude <noreply@anthropic.com>
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 22:06:53 +05:00
Myasnikov Daniil 599ace69fd test(tests): guard the COZYTEST_TRACE switch against silent regression
The quiet unit lane rests on one invariant: run_one tees the raw stream
to $log and the fail handler prints all of it for whichever test fails,
so suppressing the live stream drops the output of PASSING tests only.

That invariant fails silently. It is exercised only when a test fails,
so a green CI run proves nothing about it - a refactor of run_one's
reader loop that also broke the dump would ship, and would be found by
whoever next needed a red unit run diagnosed, with no trace and the run
already lost. Nothing in the tree covered it.

Seven tests, fixtures that fail on purpose, following the structure of
hack/cozytest-capture-gate.bats:

  - quiet mode streams no trace for a passing suite
  - the unset default stays verbose, which is what the e2e call sites in
    packages/core/testing/Makefile rely on
  - a failing test still dumps its whole trace under quiet mode, xtrace
    included, which is the load-bearing one
  - quiet mode still fails the run
  - the per-test start/end markers survive, so a hang still names the
    test that never returned
  - the unit recipe asks for the quiet runner
  - the e2e recipes do not silence theirs

Every one is mutation-proven: removing the fail-handler dump, flipping
the default, making the switch a no-op, dropping the variable from the
unit recipe, adding it to an e2e recipe, and swallowing the exit code
are each caught by exactly one test. The last of those is worth a note -
it disables the mechanism the outer runner uses to report failure, so
the run exits 0 while the failure is in its output. A probe keyed on the
exit code alone reads that as uncaught.

Two traps are recorded in the file's header rather than left to be
rediscovered. Fixtures are built with printf because the awk transform
harvests a heredoc's own @test line into the suite that contains it. And
the default is asserted under `env -u COZYTEST_TRACE`, because the
Makefile passes the variable as a command prefix, which exports it - a
nested runner started from a suite that `make unit-tests` is running
would otherwise inherit 0 and the default would never be under test.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 22:06:33 +05:00
Myasnikov Daniil 7741843178 test(e2e): order previous-instance log capture by restart recency
The container cap (COZY_PREVLOG_MAX=12) is spent in namespace order, and
"unrelated" has meant only "in another namespace" - time was never a
key. So a container that restarted once during cluster bring-up
competed on equal footing with one that crash-looped during the test,
and won whenever the pod list reached it first. Two measured e2e jobs
spent 1034 and 1545 lines on virt-api and cdi-apiserver, restarted once
at install time by `Unable to create certwatcher` hours before any app
suite ran; on a development cluster 178 containers carried a restart, their
timestamps spanning 24 days, so the cap binds hard enough for the order to
decide everything.

Ordering, not filtering, and deliberately: an install-time restart IS
the explanation when the install is what broke, and this capture has no
reference point for "when the test began" that would make a cutoff
honest. Namespace stays the primary key - a stale restart in the
namespace under test beats a fresh one three namespaces away. So this
fixes cap displacement, not the raw volume of any single run.

lastState.terminated.finishedAt joins the row, guarded so a missing
lastState cannot error the list call and fail the capture open (verified
against a live cluster: no <no value>, empty where the kubelet has
already dropped it). Reading it also fixes a latent bug the sixth field
would otherwise have caused - `read -r ... restarts` folds a trailing
field into the last variable, so the dump header would have printed
`restarts=1|2026-08-22T09:06:00Z`. It now names the time instead, which
is the first thing that decides whether the dump is worth reading.

Sorting is lexicographic over fixed-width RFC3339 UTC, so no date
parsing: reverse puts newest first and leaves undated rows last, and -s
keeps a node reboot's simultaneous restarts in input order so the
selection is comparable between runs. Rows with no timestamp pass
through untouched, which is what makes this a no-op for the existing
five-field fixtures.

Five tests added; both mutations are caught (a passthrough
sort_recent, and dropping -s - the latter by the pre-existing namespace
test). capture-previous-logs 39/39, cozyreport 184/184,
bats-no-exit-trap 22/22.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 22:06:33 +05:00
Myasnikov Daniil cb60f743d3 test(ci): extend the backport label contract to both spellings
The concurrency-group key and prepare's `if:` gate now name four labels
rather than two, and release-freeze-contract.bats pinned the pair on
both sides — as a literal and as an exact count. Every namespaced name
appended to the workflow therefore reported as a widening, which is the
right default: the count exists because the literal is a substring check
and an appended exclusion satisfies it unchanged.

Here the appended names are not a widening. `kind/backport` and
`kind/backport-previous` are genuine backport requests, so they belong
on the same side of the concurrency split as the legacy pair, and the
gate has to admit them or the rename stops the bot firing. Both counts
go to four and both literals name all four operands, so a half-finished
rename still reports.

Marked TRANSITIONAL at each site, matching the comment in backport.yaml:
when dosubot's PR labelling is switched off and the legacy arms are
dropped, these go back to the pair in the same pass.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 13:08:16 +05:00
Myasnikov Daniil fdb2e89fe7 test(unit): make the bats trace stream opt-in, quiet on the unit lane
hack/cozytest.sh runs every @test body under `set -x` and streams the
trace live. Across the ~40 hack/*.bats unit suites that is 233,444 of
238,794 lines - 98% of a 31 MB job log - and the single failing
assertion sits at line 238,573 of it. The web log viewer is unusable at
that size, and anyone (or anything) reading the job to find out what
broke pays for the whole file first.

Nothing in that stream is load-bearing on the unit lane. run_one
already tees the raw output to $log and the fail handler dumps all of
it for whichever test fails, so suppressing the live stream drops the
output of PASSING tests only.

Add COZYTEST_TRACE, default 1 so the e2e call sites in
packages/core/testing/Makefile keep the live stream - there it is the
only progress signal, and the only record at all when a step timeout or
a dead runner kills the job before the fail handler can run. The
bats-unit-tests target sets 0; running the runner by hand stays
verbose, which is the case where a human is actually watching.

Measured on 25 suites locally, quiet vs verbose: 29,528 -> 951 lines,
3.9 MB -> 94 KB, and 71s -> 48s wall, the last because the per-line
formatter forks date twice for every line it prints. Quiet output is
byte-for-byte the non-trace subset of verbose, and a hanging test still
shows as a ╭ with no matching ╰.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 12:19:17 +05:00
scooby87 9dd2841c74 feat(clickhouse): add configurable version parameter (#3476)
## What this PR does

The ClickHouse server and Keeper images were hardcoded to `24.9.2.42` in
the chart templates, so the chart exposed no top-level `version` values
key. The Cozystack API returns a managed database's engine version by
passing the HelmRelease's `spec.values.version` straight through into
the Application object, so ClickHouse instances carried no
`spec.version` in the API — unlike postgres, mariadb, mongodb,
opensearch, rabbitmq and redis, which all ship a `version` parameter.
This closes the ClickHouse half of the gap tracked in #1246.

This adds the same version-selection scaffolding the other
managed-database charts already use:

- `files/versions.yaml` maps a `major.minor` key to the full image tag,
shared by the ClickHouse server and Keeper images (both are pinned to
one version).
- `templates/_versions.tpl` (`clickhouse.versionMap`) resolves the tag
and fails the render on an unsupported version.
- `values.yaml` gains a `version` enum (`v25.8`, `v25.3`, `v24.9`),
consumed by `templates/clickhouse.yaml` and `templates/chkeeper.yaml`.
- `hack/update-versions.sh` + `make update` refresh the map from Docker
Hub, keeping only tags published for *both* the server and Keeper images
and preserving the current default so a regeneration never bumps the
major of existing deployments.

The default stays `v24.9` → `24.9.2.42`, so existing installations
render byte-for-byte identically and are never upgraded to a new major
implicitly. The generated `values.schema.json`, `README.md`, the Go API
type and the `clickhouse-rd` `openAPISchema` are regenerated with
`cozyvalues-gen` v1.6.0. A new `tests/version_test.yaml` helm-unittest
suite covers the server/Keeper mapping for each version and the
schema-level rejection of an unsupported value.

### Downstream repositories

The diff changes `packages/apps/clickhouse/values.schema.json` by adding
a new `version` field with an `enum`. Per the trigger map, this reaches
`terraform-provider-cozystack`, whose ClickHouse resource is
hand-written and would need a matching `version` attribute with a
`stringvalidator.OneOf` list and its expand/flatten pair. There is no
existing PR or issue there for it yet. This is left for a maintainer
decision rather than a speculative cross-repo PR, so the box below is
intentionally left unticked and flagged here. No other downstream
repository is reached (the website reference page is regenerated from
`README.md` by the release docs bot; ClickHouse is already in its app
list).

- [ ] 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: needs a `version` attribute on the ClickHouse resource
(maintainer decision — see note above)
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
feat(clickhouse): add a configurable `version` parameter (v25.8, v25.3, v24.9; default v24.9) so the deployed ClickHouse version is selectable and returned through the API. Existing installations are unchanged.
```


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

* **New Features**
* Added a configurable ClickHouse version setting for both server and
Keeper images, defaulting to `v24.9`.
* Supported versions are `v25.8`, `v25.3`, and `v24.9`, with compatible
image patches selected automatically.
* **Documentation**
  * Documented the version setting, default, and supported values.
* **Validation**
* Unsupported versions are rejected with an error listing allowed
options.
* **Tests**
* Added coverage for version mapping, defaults, invalid values, and
update behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 17:45:53 +03:00
Timofei Larkin f5b957ee27 feat(kubernetes)!: manage worker pools as KubernetesNodes, adopt on upgrade (Phase 2, breaking) (#3315)
## What this PR does

Phase 2 of the Kubernetes-app split
([cozystack/community#8](https://github.com/cozystack/community/pull/8))
— the **breaking** half. Stacked on #3314 (review that first).

Removes worker node pools from the `kubernetes` chart (they moved to
`kubernetes-nodes` in #3314) and adopts existing pools on upgrade:

- `kubernetes` chart is now **control-plane only**: drops the pool
`range` block, the `talos-reconcile` Job + RBAC (deleted; moved to
`kubernetes-nodes`), the preserve-old-KMT block, the
`kubernetes.nodeGroups` helper, and the per-group dashboard RBAC.
- **`spec.nodeGroups` is removed from the `Kubernetes` CR** (BREAKING).
Worker pools are now separate `KubernetesNodes` resources.
- **Migration 54** (bumps `targetVersion` to 55): pre-upgrade hook that,
for every `kubernetes-<cluster>` release, creates one
`kubernetes-nodes-<cluster>-<pool>` HelmRelease and re-annotates the
existing worker objects onto it with `helm.sh/resource-policy=keep`, so
the now control-plane-only parent upgrade does not prune running
workers. The default `md0` (implicit when `nodeGroups` was empty) is
materialised explicitly. Idempotent. Genuine infrastructure/read
failures fail closed (`exit 1`, retried on the next upgrade); hostile
stored shapes (a pool name over Helm's 53-char release-name cap, an
RFC-1123-invalid or whitespace/slash map key, a non-object `nodeGroups`)
are warned, pinned, and skipped rather than deadlocking the entire
fleet's upgrade.
- The `ingressNginx` addon no longer auto-provisions a node (the
implicit `md0` default is gone). If enabled without a pool carrying
`roles: [ingress-nginx]`, the controller DaemonSet simply schedules
nothing (degraded, not failed) — the requirement is documented in the
chart NOTES.txt rather than enforced at render time.

Because the child chart renders the pool objects **byte-identically** to
the old parent render (golden parity in #3314; the
KubevirtMachineTemplate content-hash name is preserved), adoption does
not churn live worker VMs.

### Migration number

`54` is contiguous with main (latest is `53`) and `targetVersion` is
`55`. The `52`/#3201 collision noted in earlier revisions has been
resolved: this branch was rebased and merged past it, so the numbering
is contiguous and `run-migrations.sh` has no gap to hard-fail on.

### Not yet done (blocking draft → ready)

- [ ] **dev e2e upgrade validation** (`cozystack-pr-test`): upgrade a
custom-`nodeGroups` cluster and a default-`md0` cluster; assert zero
worker-VM churn (KMT hash, Machine uid/creationTimestamp stable) and
that the adopted child HRs reconcile.
- [ ] downstream trigger-map walk (website docs for the `nodeGroups` →
`KubernetesNodes` change; terraform-provider for the removed API field).

### Downstream repositories

Deliberately unticked (draft) — the breaking API change affects docs and
the terraform provider; follow-ups to be filed before ready.

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

```release-note
Worker node pools of a managed Kubernetes cluster are now managed as separate `KubernetesNodes` resources. `spec.nodeGroups` on the `Kubernetes` CR is removed; existing pools are adopted automatically on upgrade.
```


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

* **New Features**
* Worker pools are now separate `KubernetesNodes` resources with their
own lifecycle.
* Added a render-time compatibility guard to prevent worker pools from
running ahead of the parent control-plane minor version.
* Improved upgrade/uninstall behavior to better preserve already-adopted
worker objects.

* **Breaking Changes**
* Removed per-pool `nodeGroups` and worker node health settings from the
`Kubernetes` chart/CR; pool configuration now lives in
`KubernetesNodes`.
* Ingress requires at least one `KubernetesNodes` pool with `roles:
[ingress-nginx]`.
* `cluster` and `storageClass` are immutable after creation; pool
`version` may lag but must not be ahead of the parent minor.

* **Documentation**
* Updated READMEs, notes, and schemas to reflect the `KubernetesNodes`
split and the immutability/version rules.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 12:06:18 +04:00
Alexey Artamonov e707475f96 fix(clickhouse): fetch tags via skopeo and fail closed on a missing pin
Address review of the version generator (hack/update-versions.sh):

- Fetch tags with `skopeo list-tags` (registry v2 /tags/list) instead of
  the Docker Hub v2 API, whose anonymous pagination caps at 1000 entries
  and then returns an error page with no `.results`, silently truncating
  the list so the frozen 24.9.2.42 fell outside the window and the run
  failed. skopeo is already in build-deps and is how the sibling
  generators (postgres, redis, mongodb, opensearch, kubernetes) fetch.
- Make a frozen default tag missing from the registry a hard error
  instead of silently falling back to a newer patch, which would move
  the default image for existing installs.
- Restore 0644 on the regenerated files (mktemp's 0600 survived the mv).
- Exclude /hack from the chart tarball via .helmignore.
- Refresh files/versions.yaml: v25.8 -> 25.8.32.4, the true latest 25.8
  patch now visible with the complete tag list.
- bats: add a missing-frozen-tag hard-error case and make the atomicity
  test reach the resolution phase after a valid default rather than
  failing at the default-in-set check.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-08-26 00:15:22 +03:00
Andrey KolkovandClaude Opus 4.8 5bcdb8cfb8 fix(postgres): let in-place restore re-bootstrap instead of wedging
An in-place RestoreJob (targetApplicationRef omitted) re-bootstraps the
Postgres app under its own name. On stock v1.6.2 it never comes back up,
for two independent reasons in the same restore patch - both surface only
in place, because a to-copy restore targets a different app.

1. Archive-vs-recovery serverName collision. The driver set the restored
   cluster's WAL-archive serverName equal to the recovery source
   (bootstrap.serverName = bootstrap.oldName = sourceServerName) while the
   chart pinned the archiver serverName to the release name. In place those
   coincide, so the cluster archived onto the source's own WAL prefix and
   barman-cloud-check-wal-archive refused to start ("Expected empty
   archive"). Fixed by giving the restored cluster a fresh
   bootstrap.newServerName, keyed by the RestoreJob UID (unique per
   restore, stable across reconciles); the chart archives there while
   externalClusters still recovers from bootstrap.serverName. This is what
   the manual CNPG strategies hit (serverName defaults to the cluster
   name); the cozy-default flow dodged it (serverName is <ns>-<app>).

2. useSystemBucket guard. The restore populates explicit S3 coordinates
   (destinationPath / s3CredentialsSecret / endpointCA) but left
   backup.useSystemBucket=true, so the chart's "bootstrap.enabled is
   incompatible with useSystemBucket" guard failed the HelmRelease upgrade
   and the cluster never re-rendered. This is what the cozy-default
   (system-bucket) flow hits on an in-place restore. Fixed by clearing
   useSystemBucket in the restore patch, since the app is now on the
   explicit-creds path. The driver's app view (postgresapp.Backup) gains
   the field, without omitempty so a false survives the merge patch.

Both are exercised only by an in-place restore; the postgres backup e2e
drove only to-copy, so neither surfaced in CI. A step 50 in-place restore
now guards reason 1 (manual-strategy flow); reason 2 is covered by a Go
unit assertion (a system-bucket in-place e2e needs the platform bucket in
CI and is left as a follow-up).

Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 17:31:53 +04:00
IvanHunters c0fcdd767c fix(migrations): discover pre-chartRef parents via inline spec.chart in the restamp
The restamp discovered unlabelled parent kubernetes releases by spec.chartRef
only, but that misses the population it exists for. The apps.cozystack.io/
application.kind label was introduced 2025-12-19 (fdca49838); spec.chartRef only
2026-01-14 (43da779ee). So any parent new enough to carry chartRef already
carries the label and is handled by the label-selected sweep, while every
genuinely unlabelled parent predates chartRef and instead carries an inline
spec.chart (chart name "kubernetes" from the cozystack-apps HelmRepository).
The chartRef-only selector therefore matched none of the real targets. Match both
shapes. This is a seamless-adoption improvement, not a data-loss fix: the chart's
own render-time guard already fail-closes (blocks the upgrade rather than pruning)
for any parent the restamp misses. Exact chart-name match plus the kubernetes-
name-prefix guard keep it from touching other apps or kubernetes-nodes children;
covered by an inline-shape restamp test and a non-kubernetes decoy test.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-25 13:50:14 +03:00
IvanHunters c6b6755eec fix(migrations): record foreign-owner skips, harden record_skip, scope restamp to kubernetes- names
Three hardening fixes on migration 56's new code:
- The foreign-owner branch in adopt_one pins the object prune-proof but does not
  adopt it (the child release can then stay Failed on the ownership conflict),
  yet it was the one warn+pin+skip path that did not record itself in the durable
  unadopted ConfigMap -- so an operator relying on that record to find stuck
  pools would miss it. Record it like every sibling branch.
- record_skip ran a bare append under set -e inside skip branches that are
  designed never to abort; a failed write (e.g. /tmp full) could deadlock the
  fleet upgrade for one tenant. Make it fail-open, matching the branches' intent.
- The restamp selector matched any release by chartRef; scope it to names
  starting with kubernetes- so it cannot mislabel (and then have the sweep skip)
  a non-standard release, keeping restamp and the sweep's contract in sync.
Also fix the previously-vacuous negative restamp test (its fixture lacked the
chartRef, so it never reached the label guard it claimed to check).

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-25 13:28:58 +03:00
IvanHunters 3d1a83f419 fix(migrations): make the unadopted-pools record publish best-effort
The durable-record ConfigMap write sat under set -euo pipefail right before the
version stamp, so a permanent failure (e.g. the record exceeding etcd's object
size limit at a very large skip count) would abort the migration and deadlock
the fleet upgrade with no path out -- even though every skipped pool was already
pinned prune-proof earlier in the sweep, so the workers are safe regardless of
whether the record lands. The record is a convenience artifact, not a safety
mechanism, so swallow its failure with a warning instead of failing closed.
Covered by a bats case that fails the publish and asserts the run still stamps.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-25 12:38:28 +03:00