Commit Graph
5907 Commits
Author SHA1 Message Date
Aleksei Sviridkin 8e48cfc8fd fix(rabbitmq): refuse contradictory TLS settings and correct the upgrade note
Two things this change got wrong about its own behaviour.

tls.disableNonTLSListeners was silently discarded whenever TLS resolved to off,
because the value was folded into the TLS condition before anything read it.
Asking to close the plaintext listeners with TLS off leaves nothing listening,
and the operator rejects the combination itself, returning "TLS must be enabled
if disableNonTLSListeners is set to true" and setting ReconcileSuccess=False.
Swallowing it in the chart was worse than what the operator does: the operator
at least records a TLSError event, while a dropped flag rendered a
healthy-looking cluster with every plaintext listener open and no trace of the
request. The likeliest way in is an operator hardening an internal broker who
sets only the flag, since tls.enabled follows external and so resolves to false
there. Fail with a message naming tls.enabled instead.

The README also claimed messaging clients are unaffected by enabling TLS. That
holds only for a client with a hardcoded port. <release>-default-user is the
Service Binding object the chart grants to the tenant, and the operator rewrites
its port key from 5672 to 5671 as soon as spec.tls.secretName is set — gated on
TLS alone, not on whether the plaintext listener stayed open — along with the
per-plugin port keys. A binding-driven client, which is the documented way to
consume it, follows that change and must speak TLS from then on, without being
able to verify the certificate until the trust anchor is obtainable. Say so.

Also record that the connection-uri annotation fans out: the operator's
annotation filter strips only kubernetes.io and k8s.io keys, so it lands on every
child object including the Secret the tenant reads.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:45 +03:00
Aleksei Sviridkin d668dfbd6a feat(rabbitmq): add opt-in switch to close the plaintext listeners
TLS so far only added encrypted endpoints; nothing closed the unencrypted ones,
and the chart had no way to express the intent. tls.disableNonTLSListeners gives
the operator that switch, defaulting to false so an upgrade never closes a
listener a client is already using — the choice to break plaintext has to be
made deliberately.

Its reach is worth stating plainly, because the natural reading is wrong in both
directions. It writes `listeners.tcp = none` into rabbitmq.conf, which is broker
level: the socket is not opened on any interface, so in-cluster clients lose
plaintext at the same instant external ones do, and there is no configuration
that closes it outside while keeping it inside. In the other direction it is not
"TLS everywhere" either — Erlang distribution on 25672 and epmd on 4369 ride the
headless Service, which the flag never touches, so inter-node traffic stays in
the clear regardless.

The flag also cannot coexist with chart-managed users. messaging-topology-operator
derives its connection as TLSEnabled() && (!connectUsingHTTP ||
DisableNonTLSListeners()), so the flag forces TLS on that side whatever the
operator's own configuration says, and it verifies the broker against
x509.SystemCertPool(), which cannot hold a per-release CA. The connection-uri
annotation is no escape, since it would point at the port the flag just closed —
so that annotation is now omitted when the flag is set, rather than advertising a
closed port. Every User, Vhost and Permission would silently stop reconciling.
Rendering fails instead, naming the conflict, and the flag stays usable for an
instance whose users and vhosts are managed outside the chart.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:44 +03:00
Aleksei Sviridkin 6f1ada4159 fix(rabbitmq): add the two-segment wildcard SAN for pod addresses
The SAN list covered every Service name in all resolvable forms but applied the
rule only to the non-wildcard entries, leaving the per-pod wildcard with .svc and
.svc.<domain> and no two-segment form. That is the form the operator itself uses:
RABBITMQ_NODENAME is rabbit@$(MY_POD_NAME).$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE)
and K8S_HOSTNAME_SUFFIX matches, so <pod>.<name>-nodes.<namespace> is the
canonical pod address rather than an incidental alias, and it is the form the
upstream API documentation shows to users.

A client dialling a pod by that name resolves it through the DNS search path,
connects, and fails only at hostname verification — which presents as a
certificate bug rather than a missing name. It would also break outright the
moment inter-node mTLS is wired through caSecretName.

Move the clusterDomain helper above the tri-state doc block while here, so each
comment sits next to the definition it describes.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:44 +03:00
Aleksei Sviridkin 2f42cd6baf fix(rabbitmq): keep the topology operator on the plaintext management port
Setting spec.tls.secretName makes RabbitmqCluster.TLSEnabled() true, and
messaging-topology-operator derives its own connection from that:

    useTLSForConnection = TLSEnabled() && (!connectUsingHTTP || DisableNonTLSListeners())

connectUsingHTTP comes from CONNECT_USING_PLAIN_HTTP, which the shipped
Deployment does not set, so the expression collapses to TLSEnabled(). The
operator would dial https://<release>:15671 and verify it against
x509.SystemCertPool() — a process-wide pool that cannot contain a per-release
self-signed CA. Every User, Vhost and Permission this chart renders from users
and vhosts would fail with "certificate signed by unknown authority".

The failure is quiet, which is what makes it worth guarding: users already
applied to the broker keep working, so nothing looks wrong, while password
rotation, new users, new vhosts and permission changes all stop taking effect.

Annotate the cluster with rabbitmq.com/operator-connection-uri on the plaintext
management port when TLS is on. extractURIandScheme reports tlsEnabled=false for
an http:// scheme, so the operator keeps using the listener the chart
deliberately leaves open — that listener is load-bearing, not merely tolerated,
and the README now says so. Only the operator's management connection is
affected; broker traffic is untouched.

Also add the two-segment <name>.<namespace> SANs for both the client and headless
Services. That form resolves through the DNS search path, so a client connecting
to amqps://<name>.<namespace>:5671 reached the broker and failed only at hostname
verification — which presents as a certificate bug rather than a missing name.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:44 +03:00
Aleksei Sviridkin b4c66f2b55 docs(rabbitmq): record the metrics port change when TLS is enabled
Enabling TLS swaps the published metrics port: the operator exposes either
prometheus:15692 or prometheus-tls:15691 and never both, deliberately, so a
scrape config selecting every port does not count each node twice. That choice
keys off TLS alone rather than off disableNonTLSListeners, so a ServiceMonitor
selecting the port by the name prometheus stops matching once TLS is on, even
though the broker keeps listening on 15692. Since tls.enabled inherits external,
this reaches existing instances on upgrade with no user action.

Re-adding 15692 through spec.override.service looks like the fix and is not.
updatePorts keeps only port NAMES the operator owns and carries their NodePort
across, so every reconcile drops the re-added entry; the override then reinstates
it with nodePort unset, which the apiserver reads as "allocate" on a
LoadBalancer. The resulting write retriggers the operator's own Service watch and
the NodePort is reallocated each round. ClusterIP escapes it only because Update
zeroes every NodePort there, leaving the patched spec equal to the live one — and
external:true, the configuration that turns TLS on by default, is precisely the
one that would loop. Trading a renamed metrics port for continuous LoadBalancer
and kube-proxy reprogramming is not a trade worth making.

So the chart renders no service override at all. The README says what changed and
how to retarget a scrape config, including that 15691 is HTTPS under the release
CA and so needs the trust anchor to verify, and the template comment records why
neither direction of override is used.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:43 +03:00
Aleksei Sviridkin a021bfa324 docs(rabbitmq): correct TLS comments that outran the code
Several comments described a shape the chart does not render. A test comment
claimed the chart carries a management cacertfile while the assertion directly
below it pins that no additionalConfig exists at all; the suite title and two
section headers advertised a management HTTPS block that is never rendered; and
a scope note named interNodeTLS, which is not a field on RabbitmqCluster — the
CRD exposes exactly secretName, caSecretName and disableNonTLSListeners.

Two tests were named for something they did not assert. The inter-node scope
test checked disableNonTLSListeners, which by this chart's own reasoning has no
bearing on port 25672; it now checks caSecretName, which is the field that would
actually configure inter-node mTLS.

State the ResourceDefinition backstop's real standing. The exclude list is
defence in depth, not the first line: promotion runs on the tenantresource label
that the lineage webhook stamps only when it resolves an ancestor, and a
cert-manager Secret has no ownerReference or Helm metadata, so it fails closed
before include or exclude is consulted. The list covers the case where that
stops holding.

Pin the dashboard Role's complete grant rather than only the absence of
known-bad names — every existing assertion there was a notContains, so a Role
that gained an unrelated Secret satisfied all of them.

Record what upgrading an existing external instance does: tls.enabled inherits
external, so the broker pods roll once when the operator mounts the new Secret,
and the instance gains a cert-manager dependency. No client breaks, but a
stateful broker restarting is not something to discover from a diff.

Drop four tests that duplicated earlier ones verbatim.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:43 +03:00
Aleksei Sviridkin 7406303701 fix(rabbitmq): skip the external certificate SAN instead of failing without a host
Requiring _namespace.host under external:true turned a working configuration
into a render error. The value derives from _cluster.root-host, which the
platform treats as optional — the ingress hostname policy skips rendering when
it is unset and the platform chart passes it through default so it can be empty
— and an external RabbitMQ is a bare LoadBalancer Service with no Ingress and no
hostname involvement. Releases in that state predate the tls field, so they
carry no tls.enabled: false to fall back on and would have failed on upgrade
with no way out. Append the SAN when a host is supplied and skip it otherwise.

Say plainly what the external SAN is worth: the chart creates no DNS record for
that name, so verifying by hostname means pointing it at the LoadBalancer
address yourself, and a client dialling the IP fails hostname verification.
Guarding the render was protecting a guarantee the chart does not deliver.

Document the one manual step when disabling TLS, which until now existed only in
a template comment nobody performing the downgrade would read: cert-manager
keeps the Secrets after Helm removes the Certificates, so the CA keeps its
publication label and a stale anchor keeps being projected until it is deleted.

Correct the tri-state helper's own comment while here. It claimed to guard a
class of input it does not need to: values.schema.json types tls as an object,
so non-map scalars are rejected by Helm before any template renders. Null is the
only shape that reaches the template, and that is what the dig form covers.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:42 +03:00
Aleksei Sviridkin d8a711bd29 refactor(rabbitmq): resolve the TLS tri-state in one place
The tri-state resolution was copy-pasted into three templates. It had already
drifted: the copy in the dashboard Role outlived the grant it used to gate and
was removed with nothing to catch that the remaining two could diverge in the
same way. Extract rabbitmq.tls.enabled and have both callers use it.

Reading .Values.tls.enabled directly also panicked with "nil pointer evaluating
interface {}.enabled" under `tls: null`. The app CR cannot produce that shape,
since structural-schema pruning replaces null with the {} default, but a
HelmRelease values override reaches the chart without pruning. Resolve through
dig on a defaulted dict so the value falls back to external instead.

Cover both with a suite that renders the cluster and the certificate chain
together across every external x tls.enabled combination and asserts they agree,
so a future divergence fails a test rather than issuing certificates nobody uses.

Drop the management ssl cacertfile line while here. It was set on the management
listener but not on AMQPS, and on neither does a CA alone verify client
certificates without verify_peer and fail_if_no_peer_cert. It bought nothing and
left the two listeners inconsistent; the operator supplies everything else once
spec.tls.secretName is set, so no additionalConfig remains.

Pin the ResourceDefinition invariants that decide whether a private key can
reach a tenant. The chart's helm-unittest suite cannot see that file, so the
label-based include, the non-empty selector and the exclude backstop are
asserted in hack/check-rabbitmq-rd-secrets.bats instead.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:42 +03:00
Aleksei Sviridkin 31101eeeaf fix(rabbitmq): drop schema guard that the aggregated API strips
values.schema.json carried additionalProperties:false on the tls object,
post-processed onto a generated file by a jq patch plus a drift guard, and a
test asserted that an unknown tls key is rejected. It is not rejected on the
path tenants use: the aggregated apiserver removes every boolean-form
additionalProperties before publishing, and its own comment states the schema
is consumed for documentation and type-checking rather than enforcement. The
constraint therefore held only for a direct helm install, while the test
documented it as a general guarantee. rabbitmq was also the only chart in the
catalog carrying it. Remove the patch, the guard and the test rather than keep
a check that does not check on the path that matters.

Also in the same area, three fixes for things that claimed more than they did:

The management ssl port, certfile and keyfile lines in additionalConfig
duplicated the operator's defaultTLSConf verbatim, which it writes as soon as
spec.tls.secretName is set. Only cacertfile adds anything outside mutual TLS.
Keep that one and record what it does not do — supplying a CA does not enable
client-certificate verification without verify_peer and fail_if_no_peer_cert,
so clients still authenticate by password inside the TLS session.

The dashboard Role computed the tri-state TLS flag and never read it, left over
from removing the Secret grant it used to gate.

A missing _namespace.host under external:true rendered as `nil pointer
evaluating interface {}.host`. Fail closed with a message naming the key: the
external DNS name is a certificate SAN, so omitting it silently produces a
certificate that fails verification at connect time instead of at render time.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:41 +03:00
Aleksei Sviridkin 56043f962b fix(rabbitmq): drop the service port override that could not remove a port
The override listed only the TLS ports, which reads as "plaintext is not
published on the LoadBalancer". It never did that. The operator rebuilds its
default port set from generateServicePortsMap() on every reconcile and only
then applies spec.override.service as a strategic merge patch keyed on `port`
(corev1.ServiceSpec tags Ports with patchMergeKey:"port"), so a partial list
can add or mutate entries but never remove them. With TLS on, the two ports it
listed are byte-identical to what the operator already generates, making the
whole block a no-op that credited itself with a guarantee it did not provide.

Remove it and say plainly what holds: amqp:5672 and management:15672 stay
published, on the LoadBalancer too when external is true. Closing them requires
spec.tls.disableNonTLSListeners, which writes `listeners.tcp = none` into
rabbitmq.conf — a broker-level switch that closes the socket on every interface
at once and drops in-cluster plaintext clients along with external ones. It
stays unset, so enabling TLS adds encrypted endpoints without breaking a
working connection; treat an external RabbitMQ as plaintext-reachable and
restrict it at the network layer if that is not acceptable.

Erlang distribution on 25672 is unaffected either way: the operator never
applies the flag to the headless Service that carries it.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:41 +03:00
Aleksei Sviridkin d6c1539545 fix(rabbitmq): stop granting key-bearing TLS Secrets to tenants
Both objects holding ca.crt also hold a private key: the leaf <release>-tls
carries the server key and the CA <release>-ca carries the CA key. RBAC has no
key-level filter, so granting either one to deliver a trust anchor delivers a
key with it — read access to the CA key is enough to issue a certificate for
anything and MITM the endpoint undetected.

Publish the CA instead: label the isCA Certificate so the CA-extraction
controller projects ca.crt alone into the key-free <release>.tenant-ca, which
tenants read through core.cozystack.io/tenantsecrets. The labels go on the CA
Certificate rather than the leaf, since projecting from the leaf would read a
Secret whose tls.key is the server key. The release label is required because
cert-manager issues the Secret, leaving no OwnerReference or Helm metadata to
attribute it by.

Back the label-only include with a name-based exclude covering the cert-manager
chain and the operator's erlang-cookie, so a mis-stamp of the trust-anchor label
cannot promote a private key. The erlang cookie is worth the entry on its own:
it grants peer-node code execution over port 25672, which disableNonTLSListeners
does not close.

Document the retrieval path and state the limits it comes with — the projection
depends on a controller that ships separately, and the CA rotates, so a bundle
copied once eventually stops verifying.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:41 +03:00
Arsolitt c3333c359d refactor(rabbitmq): switch CA and leaf private keys to ECDSA P-256
Replace implicit RSA key generation with explicit ECDSA P-256 for both the
CA and leaf Certificates. ECDSA P-256 provides equivalent security with
smaller keys and faster TLS handshakes.

CA privateKey: algorithm ECDSA, size 256, rotationPolicy Never (unchanged).
Leaf privateKey: algorithm ECDSA, size 256, rotationPolicy Always.

Also migrate tri-state detection in certmanager.yaml from
hasKey+ne nil to the canonical kindIs "invalid" pattern.

Assisted-by: LLM
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:40 +03:00
Arsolitt 6ac3b31a1a refactor(rabbitmq): drop null from tls.enabled schema, use kindIs invalid pattern
cozyvalues-gen v1.5.0 with {*bool} annotation emits "type": "boolean" (not
["boolean", "null"]), making the manual jq patch unnecessary. Remove the
nullable patch and its precondition check from the Makefile.

Update tri-state detection in certmanager.yaml and rabbitmq.yaml to use the
canonical kindIs "invalid" pattern, which correctly handles both the
missing-key and explicit-null cases without hasKey+ne nil.

Assisted-by: LLM
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:40 +03:00
Arsolitt d1e0d7ca24 fix(rabbitmq): drop plaintext ports from LoadBalancer override when TLS on
When both external and TLS are active, the public LoadBalancer was exposing
plaintext amqp (5672) and management (15672) alongside the TLS equivalents.
Restrict the LB override to amqps (5671) and management-tls (15671) only.

In-cluster ClusterIP plain listeners are intentionally left open to support
client migration without disruption. spec.tls.disableNonTLSListeners is not
set to avoid closing plain ports at the broker level.

Assisted-by: LLM
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:40 +03:00
Arsolitt 3d55017037 feat(rabbitmq): grant tenant read access to TLS Secret for ca.crt retrieval
Add the <release>-tls Secret to the dashboard Role resourceNames when TLS
is active, allowing tenants to retrieve the CA certificate via kubectl.
The tri-state logic in dashboard-resourcemap.yaml mirrors the same pattern
used in certmanager.yaml and rabbitmq.yaml.

Also include rabbitmq-{{ .name }}-tls in cozyrds secrets.include so the
dashboard surfaces the TLS Secret alongside the default-user credential.

Assisted-by: LLM
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:39 +03:00
Arsolitt 867cdf2c7b fix(rabbitmq): add inter-node service SANs to TLS cert
Adds wildcard and base SANs for the <release>-nodes headless service
required by RabbitMQ Cluster Operator for inter-node TLS handshake
(Erlang distribution between pods). Without these entries, hostname
verification fails during cluster formation.

Also quotes all dnsNames entries to handle the wildcard (*) prefix,
which is otherwise rejected as an invalid YAML anchor character.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:39 +03:00
Arsolitt 1d1778d769 test(rabbitmq): update cert SANs to cozy.local, add custom-domain and LB port tri-state coverage
Update certmanager_test.yaml SAN assertions to match the new default
cluster domain (cozy.local) and add a test that verifies a custom
_cluster.cluster-domain propagates into the FQDN SAN.

Add a tri-state LoadBalancer port override test (tls.enabled unset +
external=true) to cover the case that was untested in the existing matrix.

Replace documentIndex: 0 in the baseline RabbitmqCluster test with
a documentSelector so the test is stable regardless of render order.

Assisted-by: LLM
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:38 +03:00
Arsolitt 42decd708e fix(rabbitmq): treat null tls.enabled as inherit-from-external and use cluster domain for cert SANs
The previous hasKey-only check treated an explicit null tls.enabled as a
real value (false), skipping the external fallback. Add a nil guard so
null is handled the same as absent — TLS inherits from .Values.external.

Replace the hardcoded cluster.local suffix in cert SANs with the
platform-injected _cluster.cluster-domain value (defaulting to cozy.local),
consistent with how clickhouse, foundationdb, mongodb, and opensearch resolve
the cluster DNS domain.

Assisted-by: LLM
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:38 +03:00
Arsolitt ae0ff9e577 chore(rabbitmq): regenerate codegen deepcopy artifacts
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:37 +03:00
Arsolitt f08db57bb2 test(rabbitmq): add TLS test coverage
Add Helm unittest cases for cert-manager resource generation and RabbitMQ TLS configuration, with a fixture file for default TLS values.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:37 +03:00
Arsolitt c6cb09e73d feat(rabbitmq): implement TLS in Helm templates
Add cert-manager Certificate and Issuer resources for RabbitMQ TLS, and wire TLS secret references into the RabbitMQ cluster manifest.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:37 +03:00
Arsolitt 8007b899e5 feat(rabbitmq): add TLS schema and generated artifacts
Extend RabbitMQ values schema with TLS configuration options, update Go API types, regenerate CRD artifacts, and add TLS fields to the rabbitmq-rd operator config.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-04 11:18:32 +03:00
scooby87 3a70f91423 fix(vm-disk): fail on storageClass/source edit of existing disk (#3969)
## What this PR does

Fixes #2985. Editing a `VMDisk`'s `storageClass` or `source` after its
DataVolume already exists was silently ignored:
`packages/apps/vm-disk/templates/dv.yaml` looks up the existing
DataVolume and, when found, reused its `spec` verbatim (because
`DataVolume.spec` is immutable in CDI). The edit therefore rendered as a
no-op — the typed CR accepted it and the HelmRelease reported success,
while the DataVolume/PVC kept the original spec and the source of truth
silently diverged from the cluster.

The template now builds the desired `source` into a shared
`$desiredSource` variable (used both for rendering and comparison) and,
when a DataVolume already exists, calls `fail()` with an explicit
message if the requested `storageClass` or `source` differs from the
immutable existing spec — mirroring the explicit size handling already
done in `pvc-resize-hook.yaml`. Storage growth is unaffected and still
flows through the resize hook.

`source` is also marked `@immutable` in `values.yaml`, so the generated
schema now carries the same `x-kubernetes-validations` transition rule
(`self == oldSelf`) that `storageClass` already had; `types.go`,
`values.schema.json` and the generated `vm-disk-rd` CRD are regenerated
to match.

### Verification

- `helm unittest` covers both the immutability guard (via a mocked
existing DataVolume through `kubernetesProvider`, since `lookup` needs a
cluster) and initial-creation source rendering for every source type —
12/12 pass.
- Verified live on a dev cluster with `helm install --dry-run=server`
against a real DataVolume: an unchanged edit renders, a `storageClass`
change fails, a `source` change fails, and a storage grow still renders
the resize hook without tripping the guard.

### Scope note

This PR only makes the drift visible and marks `source` immutable. Two
deeper platform findings sit outside its scope: (1) #2657 (pre-existing)
— the `apps.cozystack.io` aggregated apiserver does not evaluate the
`x-kubernetes-validations` CEL rules on update, so `@immutable` is a
no-op at admission for every Application kind, which is exactly why the
chart-level `fail()` — not the CEL rule — is the substantive guard here;
(2) #3971 — that same apiserver does not honour `--dry-run=server` and
persists the change (surfaced while testing this fix).

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the diff
file by file. The diff touches only `packages/apps/vm-disk/` (template,
`values.yaml`, regenerated `values.schema.json`, tests), the regenerated
`packages/system/vm-disk-rd/cozyrds/vm-disk.yaml`, and
`api/apps/v1alpha1/vmdisk/types.go`. No package was added, renamed or
removed; no `values.schema.json` field was added, renamed or removed
(only a validation rule on the existing `source` field); no default
changed; `README.md` is unchanged; the `cozyrds` file was regenerated,
not renamed. The terraform provider models fields, defaults and enums
but not CEL immutability, so it is unaffected. No downstream repository
is reached by this change.

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

### Release note

```release-note
fix(vm-disk): editing storageClass or source on an existing VMDisk now fails with a clear message instead of being silently ignored; delete and recreate the disk to change these immutable fields.
```


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

* **New Features**
* Added support for blank, HTTP, image, disk, and upload VM disk
sources.
* VM disk sources and storage classes are now protected from unintended
changes, while storage expansion remains supported.
* Existing disks preserve their configured source and storage settings.

* **Bug Fixes**
  * Invalid or conflicting source configurations are rejected.
* Prevented silent no-op updates and incorrect immutability errors for
existing disks.

* **Documentation**
* Documented source and storage class immutability, including the need
to recreate disks when changing either setting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-02 18:40:24 +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
Andrey KolkovandClaude Opus 4.8 99abee5da3 build(redis-operator): build the operator image in CI
The valkey readiness fix is a source patch on the operator image
(patches/valkey-cli-auth.diff), but nothing rebuilds that image: the
root build target has no redis-operator entry, and hack/build-matrix.sh
derives the CI build set from those entries, so e2e runs the hand-pinned
pre-patch digest and the patch ships zero bits. Register the image build
so CI rebuilds the operator from source and the patch reaches the tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-02 14:18:53 +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 -->
v1.7.0-alpha.1
2026-09-02 10:44:23 +05:00
myasnikovdaniil 5688b3c66a fix(ingress-nginx): re-pin to the segfault-free v1.11.5 rebuild (#4012)
Re-pins the ingress controller to the rebuilt `v1.11.5`. The digest
pinned before this segfaults.

`pb.c` in lua-protobuf violates strict aliasing and gcc 14 at `-O2` acts
on it, so `pb.so` in the previously pinned build makes nginx `cache
manager` and `cache loader` children segfault continuously, tens per
second and it never stops. Workers are untouched, so the pod stays
`Ready` with `restarts=0` and nothing surfaces until master itself dies,
which is how it got here unnoticed. Fixed in
cozystack/ingress-nginx-with-protobuf-exporter#7, tag `v1.11.5`
re-pointed at that merge and both images rebuilt.

Verified against the published image, not a local build:

* `luajit -e 'require "protoc"'` exits 0, was 139.
* 25 second nginx run on a real generated config: 0 `signal 11`, was
105. 0 emerg, all four lua modules load.
* `luarocks` and `gcc` are gone from the runtime image now, so `apk` no
longer upgrades musl underneath the nginx the base image already built.

The exporter is re-pinned too, because one tag builds both images and
the rebuild published a new exporter digest. Its source did not change.

`release-1.5` and `release-1.6` pin v1.11.2 and were never affected, so
nothing to backport.


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

## Summary by CodeRabbit

* **Chores**
* Updated container image versions for the ingress controller and
protobuf exporter to newer verified builds.
  * Existing image tags and configuration remain unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-02 10:30:11 +05:00
Myasnikov Daniil 127cc2ce9a fix(ingress-nginx): re-pin to the segfault-free v1.11.5 rebuild
The v1.11.5 tag in cozystack/ingress-nginx-with-protobuf-exporter now points
at the merge of its #7, which builds lua-protobuf with -fno-strict-aliasing.
The digest pinned here was the build before that, whose pb.so makes nginx's
cache manager and cache loader children segfault continuously: tens per
second, indefinitely. The workers are unaffected, so the pod stays Ready with
no restarts and nothing surfaces until the master itself segfaults, which is
how this reached main unnoticed.

Verified against the published image rather than a local build. The one-line
repro, luajit -e 'require "protoc"', exits 0 where it exited 139, and a 25
second nginx run on a generated ingress-nginx config produces no signal 11
where the pinned build produced 105.

The exporter is re-pinned alongside it because one tag builds both images, so
the rebuild published a new exporter digest too. Its source did not change.

release-1.5 and release-1.6 pin v1.11.2 and were never affected.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-09-01 18:28:04 +05:00
scooby87 dc2e427262 docs(etcd): note that affinity is not applied to existing members (#3964)
## What this PR does

Documentation follow-up to #3962. Both reviewers (CodeRabbit and
@androndo/@IvanHunters) noted that the new `affinity` value is not
applied retroactively: the etcd-operator sets `spec.affinity` only when
it creates or replaces a member Pod and never reschedules running
members. On a cluster that already has co-located members (the exact
failure #3962 targets) the default soft rule — or a newly set hard rule
— takes effect only as each member is replaced.

This adds that caveat to the `affinity` parameter documentation in
`values.yaml` and regenerates `values.schema.json`, `README.md`, and the
embedded `openAPISchema` in `etcd-rd`. No template or behaviour change;
`make test` passes (20/20).

The other inline note (the soft podAntiAffinity default overlaps the
soft `topologySpreadConstraints`) was explicitly flagged as "fine to
keep as-is", so no code change is made for it.

> Note: this PR is stacked on `fix/etcd-soft-antiaffinity` (#3962) and
will retarget to `main` once that merges.

### Screenshots

n/a — no UI changes.

### Downstream repositories

Doc-only change to an app chart (regenerated artifacts). No package
added/renamed/removed, `kind`/`plural` unchanged, no installer
values/assets/variants touched. No downstream follow-up required.

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

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

* **Documentation**
* Clarified that etcd affinity rules apply when member Pods are created
or replaced.
* Noted that running members are not rescheduled retroactively; the rule
takes effect as each member is replaced.
* Updated the guidance consistently across configuration documentation
and schema descriptions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-01 13:56:18 +03:00
Aleksei Sviridkin c8d073fff7 fix(api): drop caller-supplied internal.cozystack.io labels in tenantsecret writes (#3599)
## What this PR does

Labels and annotations under `internal.cozystack.io/` belong to the
platform. The lineage webhook stamps
`internal.cozystack.io/tenantresource` to decide whether a Secret is
visible through this API at all, and the cacert controller stamps
`internal.cozystack.io/tenant-ca`, which an `ApplicationDefinition`
selects on in `spec.secrets` to hand a trust anchor to the tenant. The
registry copied every caller-supplied key onto the backing Secret as is,
so anyone with write access to `tenantsecrets` could plant either marker
on a Secret of their choice, or strip one to take an object out of the
platform's view.

`tenantToSecret` is the only place the write verbs go through, PATCH
included, because `rest.Patcher` is `Getter`+`Updater` and the endpoint
installer serves PATCH out of `Update`. It now skips keys with that
prefix. The update target starts as a copy of the stored Secret, so a
platform key cannot be removed either. The guard is on the whole prefix,
so a platform key added later is covered without anyone touching this
code again.

Reads are unchanged. A tenant can see the platform's verdicts, it just
cannot write them. Keys are dropped silently instead of rejected,
otherwise a GET, edit and PUT round trip would break `kubectl edit` for
no gain.

Nothing writes through this API today. Every `tenantsecrets` grant in
`cozystack-basics` is `get`/`list`/`watch`, and the console only lists
and watches. That read-only RBAC is what keeps the hole inert, and it
lives in a different package from the registry that implements the write
verbs, which is why I would rather close it here than keep relying on
it.

Two things I found next to the diff and did not fix. The `Patch` method
on this type is never reached: its signature is the `client-go` shape
and matches no interface the endpoint installer asserts, so no request
lands there. I gave it the same guard instead of leaving the weaker of
two doors, but deleting it is probably the better answer and I would
rather that call was yours. A repair after the write is weaker than the
conversion anyway: the forged key is live between the patch and the
repair, and forcing the object back to a pre-patch snapshot drops a key
that a controller stamped inside that window. Second one, and the reason
none of this is easy to spot: `Update` passes `nil` where the handler
hands the storage the current object, so `hasUID` reports false and the
patch mechanism takes its create-new-object branch. For JSON Patch,
merge patch and strategic merge patch that branch returns `NotFound`, so
all three get a 404 against an existing TenantSecret. Apply is the
exception. `applyPatcher.createNewObject` builds a fresh object of the
kind and applies onto that, so an apply does reach `Update`, carrying
only the fields the caller sent, and the merge in `tenantToSecret` is
what keeps the stored labels. Passing `secretToTenant(previous)` instead
of `nil` is the fix and it belongs in its own change, since it makes the
other three patch types live. Tell me if you want issues for either.

Closes #3464

### Screenshots

Not a UI change.

### Downstream repositories

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

Walked the trigger map against the diff: two Go files under
`pkg/registry/core/tenantsecret/`, no package added, no chart, values
schema, CRD, `ApplicationDefinition` field, namespace, `hack/` target or
release asset touched. Nothing in the map fires.

### Release note

```release-note
fix(api): TenantSecret writes no longer accept caller-supplied `internal.cozystack.io/*` labels and annotations, so a principal with write access to the API can neither plant nor strip the platform markers that decide tenant visibility on the backing Secret.
```


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

* **Bug Fixes**
* Strengthened protection of platform-managed metadata during
tenant-secret creation and updates.
* Prevented tenant-provided platform-owned labels and annotations from
overwriting protected values.
  * Preserved standard metadata while hiding sensitive resource markers.
* Improved patch-based update handling and protected metadata
consistency across changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-01 14:53:10 +04:00
Andrey KolkovandClaude Opus 4.8 ec0785fe2a fix(redis-operator): use REDISCLI_AUTH for valkey-cli auth in generated scripts
The freshworks-oss operator authenticates the CLI in its generated
readiness and shutdown scripts by exporting the engine's auth env var
before invoking it. For the Valkey engine it used VALKEYCLI_AUTH, but
valkey-cli honours only REDISCLI_AUTH (its --help documents that name and
no VALKEYCLI_AUTH). With auth enabled the readiness probe's valkey-cli
call therefore returned NOAUTH, the role never resolved, and the data
pods never became Ready.

Patch valkeyEngine.CLIAuthEnvName to REDISCLI_AUTH so auth-enabled Valkey
instances pass readiness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-01 14:41:32 +04:00
Aleksei Sviridkin df20f69e5b chore(ci): re-run with diagnostics enabled
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 13:40:46 +03:00
Aleksei Sviridkin ef4f5b45ab fix(api): drop caller-supplied internal.cozystack.io keys in tenantsecret writes
Labels and annotations under internal.cozystack.io/ are platform
verdicts, not tenant metadata. The lineage admission webhook stamps
internal.cozystack.io/tenantresource to decide whether a Secret is
visible through this API at all, and the cacert controller stamps
internal.cozystack.io/tenant-ca, which an ApplicationDefinition
selects on in spec.secrets to promote a trust anchor to the tenant.
The registry copied every caller-supplied key onto the backing Secret
verbatim, so a principal holding write access to tenantsecrets could
plant either one on a Secret of its choosing, or strip one to take an
object out of the platform's view. Only read-only tenant RBAC, which
lives in a different package, stood between a caller and that path.

tenantToSecret is the choke point every write verb goes through,
PATCH included: rest.Patcher is Getter+Updater, so the endpoint
installer serves PATCH out of Update. It now skips the platform keys,
and since the update target starts as a copy of the stored Secret,
ignoring the caller there also means a platform key cannot be
removed. Guard on the whole prefix rather than on a list of keys, so
a platform key added later is protected without anyone remembering to
come back here.

Delete the Patch method on this type. Its signature matches no
interface the endpoint installer asserts, so no request has ever
reached it, and as a write path it was strictly weaker than the
conversion: the raw patch hit the backing Secret before any guard
ran, leaving a forged key live in etcd -- visible to every controller
and to List -- until a second write put the platform keys back, or
for good when that second write failed. One door is better than two,
and the surviving one commits once.

Keys stay visible on read. A tenant may see the platform's verdicts,
and failing a write that echoes them back would break a
read-modify-write round trip for no gain, so they are dropped
silently instead of rejected.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 13:40:37 +03: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
Alexey Artamonov 6fdb5e0840 docs(etcd): note that affinity is not applied to existing members
The operator applies spec.affinity only when it creates or replaces a member
Pod; it does not reschedule running members. Document that on the `affinity`
value so operators do not expect the default (or a newly set rule) to
redistribute already co-located members until each is replaced.

Addresses the non-blocking review notes on #3962.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-01 12:47:46 +03:00
Andrey KolkovandClaude Opus 4.8 daa867c545 fix(valkey): label RedisFailover with operator-group so it is reconciled
The freshworks-oss redis-operator filters its RedisFailover List and
Watch with a server-side selector on OPERATOR_GROUP_ID (cozystack). A
RedisFailover without the matching operator-group label is never seen by
the operator, so no data pods, sentinels, PVCs or services are created.
Match the redis app chart, which already sets the label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-09-01 13:22:29 +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 8d386c2cbc docs(mongodb): name the trust anchor as the tenant sees it
Every object this chart creates is named after the Helm release, not
after the MongoDB resource, so a MongoDB named catest yields
mongodb-catest.tenant-ca. The retrieval commands used a bare <release>
placeholder, which reads as the resource name and produces a command
that returns nothing. Spell the prefix out in the two commands meant to
be copied, and define the placeholder once for the prose.

Split the sentinel's shape test in two. The absence of ownerReferences
is the invariant — it breaks the walk from the projection back to the
release, and the tenant stops receiving the anchor. An explicit
namespace or a chart-added label breaks nothing by itself; grouping all
three under one name presented them as equally load-bearing.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:34 +03:00
Aleksei Sviridkin 04112dacdf test(mongodb): stop the tenant-Role guards passing on the wrong document
The assertions withholding the key-bearing TLS Secrets from the tenant
Role addressed it as document 0 and, in two cases, asserted only with
notContains. Neither survives a rearrangement.

notContains passes when its path does not resolve, so a test built from
it alone goes green against the wrong document. Reversing the order of
the two documents in the template moved the RoleBinding into position 0,
and "does not grant the key-bearing TLS secrets when external is
enabled" and "does not grant the tenant-ca projection by name" both
stayed green while the Role granted <release>-ca-cert by name — the
Secret holding the CA private key.

Select the Role by kind, and pair every notContains with a positive
assertion on the same path so a rule reordering that moves rules[1]
reddens too. Indexing survives neither rearrangement; selecting survives
the first; the positive anchor catches the second.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03: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 fc57e928e1 test(mongodb): drop a document count the sentinel invalidates
The cert-manager exclusion suite justified its `any: true` requirement
with a specific number of rendered documents. The argument only needs
"more than one"; the number was already tied to one value set and the
sentinel adds another document.

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 4092f59528 docs(mongodb): scope the upgrade-availability claim to quorum-capable sizes
The upgrade-restart note said the replica set stays available through the
rollout. That holds at three members and fails below them, and the chart
permits both smaller sizes: replicas 1 and 2 render unsafeFlags.replsetSize
and are not excluded from the SAN change, so they gain horizons and roll on
upgrade exactly as a three-member release does. At one replica the rollout
takes the only mongod down; at two the write majority is still two, so
restarting either member stalls writes until it returns. An operator running
a single-replica external release would have read the old text and declined
to book a window.

Cover the two replicas case, which was untested, and assert horizons and the
sub-quorum marker together at both sizes, so gating horizons to sizes that
can absorb a restart turns the tests red and sends someone back to this
paragraph. The default-size case carries the pair as a positive control: the
marker proves nothing unless it is absent where a restart is survivable.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:43 +03:00
Aleksei Sviridkin 7ef09cc963 docs(mongodb): show how to verify a connection against the trust anchor
The CA retrieval command was documented without the other half: what to do
with the bundle once it is in hand. A tenant reading only the retrieval step
has the certificate and no worked example of a verified connection.

Pass the two TLS inputs as mongosh flags rather than folding tls=true into
the connection string, because the correct separator differs by topology --
the replica-set URI already carries a ?replicaSet=rs0 query and the sharded
one carries none, so a single copyable form would be wrong for one of them.

Call out that omitting the flags is silent. Under preferTLS the server
accepts the plaintext connection and nothing reports the downgrade, so a
working connection is not evidence that TLS was negotiated.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 8af39eea22 docs(mongodb): correct the external SAN version gate and the leaf lifetime
The external-hostname note claimed the operator and the CR version were "both
shipped by this chart". Only crVersion is: the PSMDB operator is a separate
platform component with its own lifecycle, and the SAN fold is gated on both.
On a platform carrying an older operator nothing reports the unmet gate — the
chart still renders splitHorizons, the CR is still accepted and the cluster
still comes up healthy, while the external hostnames never enter the SAN list
and a verifying client fails the handshake. State which half the chart owns and
point at the operator version as the thing to check.

The leaf certificate lifetime was stated as a flat 90 days. It is an operator
default this chart deliberately does not pin: nothing in the tenant contract
keys off it, the operator reissues without client action, and pinning would
freeze the chart against upstream shortening it. Attribute the number to the
operator version and pin the field's absence, so "inherited rather than chosen"
stays falsifiable — the same treatment allowInvalidCertificates already gets.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 90eb5c6815 test(mongodb): make the cert-manager exclusion suite discriminate
Every assertion in the suite passed while checking nothing. containsDocument
without `any: true` asserts that EVERY rendered document matches, so negating
it asks whether "not every document is a Certificate" — true of any chart
rendering more than one object, and this one renders eight. A cert-manager
Certificate appended to templates/mongodb.yaml, the most plausible place for
one to appear, left the suite fully green.

Add `any: true` to each containsDocument so the negation means "no document
anywhere is of this kind". Unscoped assertions are ANDed across template
files, which is what makes the negatives correct: they go red if any file
renders the forbidden kind.

Add a positive control asserting the chart does render the PerconaServerMongoDB
CR through the same matcher shape, scoped to the one template that emits it.
The four negatives are all satisfied by a matcher that never matches, which is
exactly how this suite failed; the control fails if that returns.

Verified by injecting a Certificate into the chart: the suite now fails and
previously passed.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +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