Commit Graph
2043 Commits
Author SHA1 Message Date
Aleksei Sviridkin b43f00d2bb feat(mariadb): publish a key-free CA projection to the tenant
The tenant needs the CA certificate to verify TLS connections, but the
cert-manager Secrets that back the chain are key-bearing: <name>-ca-tls
holds the CA private key and <name>-tls holds the server private key.
The dashboard Role granted the tenant read access to both whenever TLS
was on, which handed out signing material where only a trust anchor was
required.

Drop both from the tenant grant and mark the CA Secret as the source of
a key-free projection instead. The chart owns the cert-manager chain
here, so it labels the CA Secret directly, inside the existing TLS gate:
with TLS disabled the chain is not rendered and nothing is marked. Only
the CA carries the labels, never the leaf.

The selector added to the ApplicationDefinition is deliberately specific.
An empty selector would compile to a match-everything label selector and
project every Secret in the namespace to the tenant.

The source Secret and the projection are matched by two different
labels on purpose: the source stays key-bearing and internal, while the
published copy is key-free. Selecting the source directly from the
consumer would defeat the split and leak the CA private key.

Note that the labels and the selector are inert until the controller
that produces the projection exists: nothing reads the labels yet, and
the selector matches no Secret.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-03 01:38: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
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
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 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
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 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 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
Arsolitt 7f8f32b1ba fix(mongodb): always enable operator-managed TLS, drop tls.enabled field
Address review feedback from lexfrei (B3): the tri-state tls.enabled field
could not actually disable TLS. The PSMDB operator defaults spec.tls to
preferTLS whenever it is unset, so tls.enabled: false rendered no spec.tls
while the operator still served TLS — a misleading no-op.

Remove the field and unconditionally render spec.secrets.ssl and
spec.tls.mode: preferTLS to match operator behaviour. splitHorizons SAN
injection now keys off external alone. Also drop the Makefile jq patch that
re-injected the tls schema property.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-09-01 11:11:27 +03:00
Arsolitt e9f276195f test(mongodb): cover TLS modes, splitHorizons, and RBAC
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Assisted-By: Claude <noreply@anthropic.com>
2026-09-01 11:10:59 +03:00
Arsolitt f6816e1f44 feat(mongodb): add operator-managed TLS with external hostname SANs
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Assisted-By: Claude <noreply@anthropic.com>
2026-09-01 11:10:55 +03:00
Andrei Kvapil 4e6313b608 feat(valkey): register Valkey application in the platform
Wire the managed Valkey app into the platform: add the valkey-rd package
carrying the Valkey ApplicationDefinition, the valkey-application PackageSource
(reusing the existing redis-operator), enable it in the paas bundle, and grant
tenant roles access to the valkeys resource.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-09-01 11:43:10 +04:00
Alexey Artamonov 06624bacd0 fix(vm-disk): fail on storageClass/source edit of existing disk
Editing a VMDisk's storageClass or source after its DataVolume already
exists was silently ignored: dv.yaml reused the existing DataVolume.spec
verbatim (DataVolume.spec is immutable in CDI), so the edit rendered as a
no-op while the HelmRelease reported success and the CR diverged from the
cluster.

When a DataVolume already exists, fail with a clear message if the
requested storageClass or source differs from what the disk was created
with. Both compare the operator's input recorded in an annotation at
creation (vm-disk.cozystack.io/storage-class and .../source), written only
at creation and preserved verbatim afterwards — never overwritten with an
unapplied edit. Recording the input rather than reading the live object
matters on both axes: the image-clone PVC name carries a chart-version
prefix (vm-image- was renamed to vm-default-images-, commit bec35e3a) that
drifts across upgrades with no edit, and the live PVC storageClassName of an
adopted or foreign DataVolume may legitimately differ from the CR default
without being a change. The annotation key presence (not its value) marks a recorded class, so a disk
created with the cluster default "" is recorded and still caught if later
changed to a concrete class.

Disks with no annotation (created earlier, or out-of-band) are handled
conservatively so a working release never deadlocks: source is compared
against the stored spec.source only when that stored source is a shape the
chart itself produces, so an adopted DataVolume using spec.sourceRef or a
non-chart source (registry, snapshot, http+secretRef, ...) is left alone
rather than deadlocked. The image-clone exemption fires only when the stored
source is a cozy-public pvc AND the request is itself an image, so a change
away from an image source to another type is still caught. storageClass has
no safe live fallback, so an un-annotated disk is not guarded here (apiserver
CEL, #2657, is the eventual guard). An unrecognized source type
now fails explicitly. Storage growth is unaffected and still handled by
pvc-resize-hook.yaml.

Mark source as immutable in values.yaml so the generated schema carries
the same x-kubernetes-validations rule already present on storageClass,
and document the contract, the exemption, and the source-annotation schema
coupling in docs/storage-immutability.md and the chart README.

Add helm-unittest coverage for the immutability guard (rejection incl.
clearing storageClass to empty and changing a non-image disk to image,
no-op reuse with annotations preserved, legacy image-disk drift, annotation
non-poisoning, un-annotated storageClass skip, adopted default-class disk,
unknown source) and initial-creation source rendering.

Fixes #2985

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-08-28 18:26:36 +03:00
Myasnikov Daniil 6d9444056d chore(keycloak): bump KMS proxy to 0.2.2
Pin the released image by digest so package rendering is reproducible.

Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-28 17:11:41 +05: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
Andrei Kvapil c8e4d97df9 feat(ingress-nginx): bump to 1.11.5 and restrict the admission webhook (#3798)
## What this PR does

Bumps ingress-nginx to chart 4.11.5 / controller v1.11.5, and closes the
admission webhook to everything except the API server.

### Version bump

The shipped `v1.11.2` controller is a cozystack rebuild that **already
carries a backport** of the CVE-2025-1974 fix (v0.28.2, March 2025), so
this is not a fix for a live vulnerability. The problem is that it
reports itself as a plain `v1.11.2 build git-d3bb2b4`: the binary is
built from the upstream 1.11.2 tag with the patch applied on top, which
does not change the version ldflags, and the image labels are inherited
from the `registry.k8s.io` base layer. A patched build is therefore
indistinguishable from an unpatched one by anything except its digest,
and anything scanning by version string flags it. This bump removes that
ambiguity.

`packages/system/ingress-nginx` feeds three consumers — the platform
ingress, tenant Ingress applications (`extra/ingress`), and the ingress
addon of tenant Kubernetes clusters — so one bump covers all three. The
protobuf-exporter sidecar moves in lockstep, since its tag tracks the
upstream controller version.

`CHART_VERSION` is now pinned in the package Makefile. `helm pull`
carried no `--version`, so `make update` would have fetched whatever is
latest upstream (4.13.9 today) against a controller image that only
exists for selected versions.

### Admission webhook policy

The webhook is only ever called by the API server, but it was reachable
from every pod in the cluster, in any namespace — avoidable attack
surface independent of any specific CVE.

The new `CiliumNetworkPolicy` is deny-only and disables default-deny. A
Cilium rule carrying an ingress section otherwise puts the selected
endpoints behind a default-deny, which here would mean enumerating every
port the controller serves (80, 443, metrics, the exporter, tcp/udp
services) and breaking traffic on the first one missed. Subtracting one
port is the safer construction.

Two callers are deliberately not denied:

* **host-network sources**, which are not Cilium endpoints — this covers
a management cluster, where the API server runs on the host network;
* **the konnectivity agent**, which is not optional. On a Kamaji-hosted
tenant cluster the API server lives outside the cluster and reaches
in-cluster webhooks through the konnectivity tunnel (Kamaji defines only
the `cluster` egress selection), so the call arrives from the agent pod
in `kube-system`, not from the API server address. A `fromEntities:
[kube-apiserver]` allow would not match it, and every Ingress
create/update in every tenant cluster would start failing.

The policy is namespaced rather than clusterwide on purpose: the same
package is installed into several namespaces of one cluster, so a
cluster-scoped object with a fixed name would collide between releases.
It can be disabled with `admissionWebhookPolicy.enabled`.

### Verification

Tested on a live Kamaji tenant cluster (k8s 1.35.6, Cilium 1.19), with
the addon enabled for the test and disabled afterwards:

| | probe from a pod in `default` | Ingress create |
|---|---|---|
| without policy | `open` | works |
| with policy | `Connection timed out` | works |

Also confirmed on the management cluster that `kube-apiserver` runs as
static pods on the host network, so the host-network carve-out holds
there.

The package previously ran no unit tests at all; a `test` target is
added with 7 assertions covering the policy. It is scoped with
`--with-subchart=false` because the vendored chart carries upstream's
own suites, which assert upstream defaults and fail under our values.
The full `hack/helm-unit-tests.sh` sweep is green.

The controller and exporter images are rebuilt by
cozystack/ingress-nginx-with-protobuf-exporter#5, which vendors the
`11843.diff` patch instead of fetching it from a still-open upstream PR
at build time.

### Screenshots

Not applicable — no UI changes.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff, which touches `packages/system/ingress-nginx/` and one comment
line in `hack/promote-retag.sh`:

* **website** — no package added, renamed or removed under
`apps/`/`extra/`; no platform component added or removed; no change to
`packages/core/platform/values.yaml`, variants, bundles or release
assets. The `package Makefiles` trigger was considered because of
`CHART_VERSION` and the new `test` target: `CHART_VERSION` does not
change the documented contract of `make update`, and `test: helm
unittest` is an existing pattern in the repo
(`packages/system/cilium-networkpolicy/Makefile`), not new tooling.
* **terraform-provider-cozystack** — every trigger is about
`packages/apps/`, `values.schema.json`, version enums or
`ApplicationDefinition`; none apply to a system package.
* **ansible-cozystack** — no change to installer values, platform keys
the role sets, variants, waited-on objects or node prerequisites.
* **ccp** — `hack/package.mk` and `hack/common-envs.mk` untouched;
nothing under `hack/` moved or renamed, and the edit to
`hack/promote-retag.sh` is a comment with no behaviour change; layout
and namespaces unchanged.
* **external-apps-example**, **talm**, **cozyhr**, **cozy-proxy**,
**cozystack-telemetry-server**, **examples** — no trigger matches.

- [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(ingress-nginx): update to chart 4.11.5 with controller v1.11.5, and restrict the admission webhook so it is reachable only by the API server. The previously shipped v1.11.2 image already contained a backport of the CVE-2025-1974 fix; it reported itself as a stock v1.11.2 build, which this update makes unambiguous.
```


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

* **New Features**
* Added optional network protection for the admission webhook with
restricted access rules.
* Added PrometheusRule support and improved ServiceMonitor
configuration.
  * Webhook service ports are now configurable.

* **Updates**
  * Upgraded ingress-nginx and controller images to version 4.11.5.
  * Improved default-backend availability and topology-spread settings.
* Added Pod Security Admission guidance and refreshed configuration
documentation.

* **Bug Fixes**
* Corrected webhook service rendering and default-backend disruption
budget behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-27 16:59:06 +02:00
Andrei Kvapil 02c6942adc fix(kubeovn-webhook): restrict the webhook port to the API server (#3955)
## What this PR does

Closes the exposure behind GHSA-g883-q79m-8225 by constraining who can
reach the kube-ovn webhook's serving port. The `/mutate-pods` handler
returns namespace-derived annotations to any caller that reaches it, and
until now nothing stopped an arbitrary in-cluster pod from being that
caller.

### Why the client-certificate path does not close it

cozystack/cozystack#2698 added `--client-ca-file`, deliberately off by
default, as observation mode. Turning it on requires the API server to
**present** a certificate, which it does only when its
`AdmissionConfiguration` names a `kubeConfigFile` for the webhook
plugins.

Checked against a running Cozystack control plane rather than assumed:
the `AdmissionConfiguration` there carries a `PodSecurity` plugin block
and nothing else, so no client certificate is offered to admission
webhooks at all. Flipping to `RequireAndVerifyClientCert` on such a
cluster would reject the API server itself, and with `failurePolicy:
Fail` that stops pod creation cluster-wide.

This matters for how the remaining work was scoped: the plan had been to
observe issuer logs for a day and then pin a CA. No amount of
observation changes the outcome, because the absence is a property of
the control plane's configuration, not of the traffic. Making
`--client-ca-file` usable is a separate, larger change to how the
platform configures the API server, and it is not a prerequisite for
closing the exposure.

### What the policy does

Denies pod traffic and world traffic to the serving port, and needs no
carve-out for the legitimate caller: the API server runs on the host
network, so it is not a Cilium endpoint and `fromEndpoints` never
matches it. Unlike the ingress-nginx admission webhook this component
exists only on the management cluster, so there is no
konnectivity-tunnelled caller to exempt either — and consequently no
forgeable pod label anywhere in the selector.

It is a `CiliumNetworkPolicy` rather than a core `NetworkPolicy`, and
that distinction is the reason it works at all:
`packages/system/kubeovn/values.yaml` sets `ENABLE_NP: false`, so
kube-ovn programs no ACLs and a core NetworkPolicy would be created and
never enforced. The values comment that claimed NetworkPolicy was simply
not an option is corrected — it was true only of the core kind.
`webhookPolicy.enabled` lets a cluster whose networking variant ships no
Cilium turn the object off.

Deny-only with default-deny explicitly disabled, matching the
ingress-nginx admission policy: putting these pods behind a default-deny
would mean enumerating every port they serve and breaking traffic on the
first one forgotten.

### Verification

Eight helm-unittest cases cover the port, the all-namespaces span, the
absence of any label-based exemption, the world deny, the disabled
default-deny, the Cilium kind, the selector and the off switch. The
rendered object was additionally accepted by a live API server via
`kubectl apply --dry-run=server`, so the CRD validates it as written.

Not verified: that a denied pod actually fails to reach the port at
runtime. That needs a live negative test against the policy, which is
the same gap the ingress-nginx admission policy carries.

### Screenshots

Not applicable — no UI change.

### Downstream repositories

Walked the trigger map against the diff. Nothing is reached: the change
is one chart template, its values and its tests under
`packages/system/`, with no package added or removed under
`packages/apps/` or `packages/extra/`, no
`packages/core/platform/values.yaml` key, no platform variant or bundle
change, nothing under `hack/`, and no node prerequisite.

- [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(kubeovn-webhook): the kube-ovn webhook's serving port is now closed to in-cluster pods and external traffic by a CiliumNetworkPolicy. It previously answered any in-cluster pod with namespace-derived annotations (GHSA-g883-q79m-8225). Disable with webhookPolicy.enabled=false on clusters whose networking variant ships no Cilium.
```


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

* **New Features**
* Added a Cilium network policy to protect the namespace-annotation
webhook.
* The policy is enabled by default and restricts access to the webhook
port, blocking unauthorized in-cluster and external traffic.
* Standardized the webhook port across its deployment, service, and
network policy configuration.

* **Bug Fixes**
* Improved webhook network isolation when standard Kubernetes
NetworkPolicy enforcement is unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 23:04:24 +02:00
Andrei Kvapil a7cef35d88 fix(kubeovn-webhook): address review on the webhook network policy
Derive the serving port from a single helper so the container port, the
Service targetPort and the policy cannot drift apart. With default-deny
disabled a disagreement failed open: the policy would deny a port nothing
serves while the real one stayed reachable, removing the control without
breaking anything visible. The knob that allowed the drift is gone and a
test asserts the three agree.

Move the rationale into template comments. Helm strips only {{/* */}}, so
the block was being copied verbatim into every rendered manifest. Record
in it what cilium monitor showed on a live kube-ovn cluster: the API
server reaches the webhook from the node InternalIP with identity host or
kube-apiserver, never from the join subnet and never as world.

Drop the egress default-deny toggle, which does nothing without egress
rules, and note that the policy removes in-cluster pods and external
traffic rather than every caller.

Assisted-By: Claude
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-26 19:01:24 +02: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
scooby87 94614c021a fix(etcd): spread members across nodes with default soft pod anti-affinity (#3962)
## What this PR does

The `etcd` chart hardcoded only a **soft** `topologySpreadConstraints`
(`whenUnsatisfiable: ScheduleAnyway`) and never rendered
`spec.affinity`, even though the `EtcdCluster` CRD supports it. On small
clusters the scheduler therefore co-locates multiple members on one
node; losing that node takes out quorum (or the whole cluster).

We hit this in production: a single node reboot left several 3-member
etcd clusters with 2–3 members on the downed node, and one tenant's
control-plane etcd lost quorum until the node came back.

This renders `EtcdCluster.spec.affinity` from a new `affinity` value:

- **Default (value left empty):** the chart applies a **soft**
(preferred) `podAntiAffinity` on `kubernetes.io/hostname`, so members
spread across nodes on a best-effort basis. Soft on purpose — a hard
rule would leave a member `Pending` when replicas exceed schedulable
nodes, or when node-local storage pins a member to a node, so it is safe
to default on.
- **Override:** set `affinity` to any rule — e.g. a hard
`requiredDuringSchedulingIgnoredDuringExecution` podAntiAffinity — to
forbid co-location outright on clusters with enough nodes and
non-node-pinned storage. A supplied value replaces the default cleanly.

The existing `topologySpreadConstraints` block (and its
`_cluster.scheduling.globalAppTopologySpreadConstraints` override) is
unchanged.

Generated artifacts (`values.schema.json`, `README.md`, and the embedded
`openAPISchema` in `etcd-rd`) were regenerated with `make generate`;
`make test` passes (18/18).

### Screenshots

n/a — no UI changes.

### Downstream repositories

Walked the trigger map against the diff: this only adds one values key
to an app chart. No package is added/renamed/removed, `kind`/`plural`
are unchanged, no installer values or release assets or variants change,
and `README.md` is regenerated here (the website bot regenerates
reference pages from it on release). No manual downstream follow-up is
required.

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

### Release note

```release-note
fix(etcd): etcd clusters now spread their members across nodes by default via a soft podAntiAffinity, and expose an `affinity` value so operators can set a hard rule; previously a single node could host a quorum of members and take out the cluster when it failed.
```


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

* **New Features**
  * Added configurable pod affinity rules for etcd deployments.
* By default, etcd members use soft anti-affinity to spread across hosts
without blocking scheduling.
  * Custom affinity settings can override the default behavior.
* Exposed affinity configuration in the etcd application schema and
dashboard.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 14:27:15 +03:00
scooby87 6bb946fa5b fix(postgres-operator): bump CloudNativePG to 1.30.0 for the Cluster scale-subresource selector (#3951)
## What this PR does

Bumps the vendored `cloudnative-pg` chart from 0.27.1 to 0.29.0
(operator + CRDs from 1.28.1-patched-to-1.28.2 up to 1.30.0) and drops
the hand-maintained `patches/cloudnative-pg-1.28.2.patch` entirely —
0.29.0 is a published chart, so the version-alignment patch is no longer
needed.

**Why this is required.** Database read-replica autoscaling (community
proposal `design-proposals/database-horizontal-autoscaling`) drives the
CNPG `Cluster` scale subresource with a stock HPA (via KEDA). That only
works when the subresource serves `status.selector`, which
[cloudnative-pg#8996](https://github.com/cloudnative-pg/cloudnative-pg/pull/8996)
added and shipped in CNPG 1.28.4 / 1.29.2 / 1.30.0. cozystack shipped
1.28.2, whose scale subresource has no selector, so the HPA fails with
`InvalidSelector`.

**Why 1.30.0 and not 1.28.4.** The floor is ≥ 1.28.4, but the upstream
chart repo publishes no chart for 1.28.4 (the 1.28 line stops at chart
0.27.1 / appVersion 1.28.1), so 1.28.4 could only be reached by
hand-patching the 0.27.1 chart's operator image and CRDs up to the
backport. A live PoC on a dev cluster proved that path is broken: the
1.28.4 operator running under the 0.27.1 (1.28.1) chart templates
crash-loops on a missing RBAC grant it needs but the older chart does
not provide — `endpointslices.discovery.k8s.io is forbidden: ... cannot
list resource "endpointslices"` — so `status.selector` is never
populated and the HPA still cannot scale. 1.30.0 (chart 0.29.0) is the
only published chart that carries the selector together with matching
RBAC/CRDs/templates, and the 1.28 line is EOL (2026-06-30), so 1.30.0
also keeps us on a supported release. cozystack uses the barman-cloud
plugin (not native barman, removed in 1.31) and no CNPG Poolers, so the
1.30 deprecations and the known pooler bug do not apply.

**Validation on a dev cluster (CNPG 1.30.0).** Operator rolls out clean
(no crashloop, the `endpointslices` RBAC error is gone); all existing
`Cluster` objects return to Ready; `/scale` now returns a populated
`status.selector` (`cnpg.io/cluster=<name>,cnpg.io/podRole=instance`);
an HPA pointed at a `Cluster` no longer reports `InvalidSelector`
(`AbleToScale=True, SucceededGetScale`); psql connectivity and streaming
replication verified.

The version-guard unittest is updated to 1.30.0 and now also asserts the
clusters CRD scale subresource carries `labelSelectorPath:
.status.selector`.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff, file by file. The change touches only the vendored
`cloudnative-pg` operator chart (CRDs, RBAC, values), the package
`Makefile`, and the version-guard test. The `packages/apps/postgres` app
and its `version` enums are unchanged,
`packages/core/platform/values.yaml` is unchanged, and the
`clusters.postgresql.cnpg.io` CRD is not one the terraform provider
types by hand (it travels as opaque JSON). No downstream repository
restates anything this diff changes.

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

### Release note

```release-note
fix(postgres-operator): bump CloudNativePG to 1.30.0 so the Cluster scale subresource serves status.selector, enabling HPA-driven read-replica autoscaling (previously the HPA failed with InvalidSelector on 1.28.2)
```


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

* **New Features**
  * Upgraded the CloudNativePG operator and Helm chart to 1.30.0/0.29.0.
* Added expanded image catalogs, pooler settings, pod selectors,
scheduling options, monitoring TLS, and LZ4 backup compression.
* Improved permissions for database roles, endpoint slices, and lease
monitoring.

* **Bug Fixes**
* Strengthened configuration validation and removed the deprecated
workload reference option.
  * Updated compatibility requirements to Kubernetes 1.29 or newer.

* **Documentation**
* Expanded installation, configuration, verification, upgrade, and
uninstall guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 13:55:11 +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 c83a22e077 fix(etcd): spread members across nodes with default soft pod anti-affinity
The etcd chart only set a soft topologySpreadConstraints and never rendered
spec.affinity, so the scheduler could place several members of a 3-node
EtcdCluster on the same node; losing that node then took out quorum (observed
in production after a single node reboot).

Render EtcdCluster.spec.affinity from a new `affinity` value. When it is left
empty the chart applies a soft (preferred) podAntiAffinity on
kubernetes.io/hostname, so members are spread on a best-effort basis without
ever leaving a member Pending on small or node-pinned-storage clusters.
Operators can override the value — e.g. a hard requiredDuringScheduling rule —
where node count and storage allow.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-08-25 23:09:40 +03:00
Alexey Artamonov e23fceffe7 docs(postgres-operator): distinguish chart version from appVersion in comment
Reword the values.yaml comment so it does not call appVersion 1.30.0 a
"published chart": the published chart version is 0.29.0 and it carries
appVersion 1.30.0. Keeping the two distinct stops a maintainer from running
`helm pull --version 1.30.0`.

Addresses CodeRabbit review feedback on #3951.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-08-25 18:52:30 +03:00
Andrei Kvapil 091d8ce143 fix(ingress-nginx): re-pin both images to the rebuilt v1.11.5
The controller digest this pinned was a build dated 2025-03-26, whose
nginx.tmpl calls buildHTTP3Listener — a function no v1.11.5 binary
defines. Go binds template functions at parse time, so the
{{ if $cfg.UseHTTP3 }} guard around it does not help and the controller
crash-loops on a default config, serving no webhook at all.

The tag pointed at that image because the controller had not been built
since March: the build in the fork failed on a luarocks step, and the
cause was outside the repository. luarocks resolves a rock name through
the luarocks.org index, and that index has grown past LuaJIT's hard
limit of 65536 constants per function, so no client can load it and
every lookup returns "No results matching query were found". Neither a
luarocks bump nor GNU wget fixes that; installing from a rockspec URL,
which skips the index, does. Fixed in
cozystack/ingress-nginx-with-protobuf-exporter#6 and the v1.11.5 tag
re-pointed at it.

Verified in the rebuilt image: zero references to buildHTTP3Listener,
and both lua-protobuf and lua-iconv present — the rocks this fork exists
to add.

The exporter is re-pinned in the same commit rather than separately. It
had drifted a build ahead of its tag before this rebuild, and refreshing
one half alone would leave the PR looking updated while the controller
was still the March build.

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-08-25 15:45:35 +02:00
IvanHunters bf38655ab5 docs(computeplane): nodeGroups allowlist drops unlisted fields, not pass-through
The nodeGroups docs still described the old denylist behaviour: "unlisted
NodeGroup fields pass through to the wrapped chart unvalidated". After the switch
to an allowlist (pick) both halves are wrong: fields go to the kubernetes-nodes
pool chart (not the wrapped kubernetes chart), and anything the pick does not name
is dropped, not passed through. A tenant that sets e.g. talos.version on a
nodeGroup gets no error and no effect (accepted-but-inert). Correct the source
@param and the hand-written README intro, regenerate schema/README/cozyrd, and
refresh the stale "omit" wording in the splice test comment to say allowlist.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-25 16:39:19 +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
Kirill Ilin 517e33f67b fix(keycloak): pin the Vault leg to the configured CA and roll on rotation
SSL_CERT_FILE alone does not narrow trust. crypto/x509 replaces only its file
list and then scans SSL_CERT_DIR unconditionally, and the pinned proxy image
ships /etc/ssl/certs/ca-certificates.crt, so the connection guarding the KEK
trusted the private CA plus every public root — a mis-issued certificate for the
Vault hostname was still accepted. Set SSL_CERT_DIR to the mount as well, which
collapses the pool to the CA the operator configured.

An unvalidated caBundle failed open on top of that: a non-PEM value is dropped
silently by AppendCertsFromPEM, leaving the proxy on the image's roots with no
error at all. Reject it at render time, the way backend and auth already are.

Rotating an inline caBundle had no effect either. The pod template did not
change, so no new pods, and crypto/x509 memoizes the pool per process — the
upgrade reported success while the old CA stayed in use until some later
restart. Derive a checksum/vault-ca annotation from the bundle so the rollout
happens with the change.

Also add vault.caSecretKey: the projected key was hardcoded to ca.crt, and a
bundle stored under another name (a cert-manager Certificate from an ACME issuer
emits no ca.crt at all) wedged the pod at mount time with no way to correct it
from values.

Drop the claim that the database leg verifies against its own CA pool: the chart
never sets KKP_BACKEND_CA_FILE, so that leg is plaintext today, as the note 30
lines above already says. The conclusion holds — it never consults the system
trust store — but for the other reason.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-08-25 18:11:40 +05:00
Kirill Ilin 98ef5413f4 fix(keycloak): keep the Vault CA options inert on the static KMS backend
The volume and mount that consume the Vault CA sit at pod level, outside the
vault-transit env block, while $vaultCASecret was seeded from caSecretName
regardless of the backend. With kms.backend=static and caSecretName set, the
Deployment mounted a Secret the chart never renders, so the proxy — the single
choke point for all Keycloak DB traffic — would sit in ContainerCreating.

Gate both CA keys, and the mutual-exclusion check between them, on
vault-transit, so the whole vault.* subtree behaves the same way under static:
ignored. Project only ca.crt from a pre-seeded Secret, which keeps a
cert-manager tls.key out of the container and turns a bundle stored under
another key into a mount failure rather than an opaque TLS error later.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-08-25 18:11:40 +05:00
Kirill Ilin 3531553176 feat(keycloak): let the KMS proxy trust a private Vault CA
The proxy could only reach a Vault whose certificate chains to a publicly
trusted root, so a Vault fronted by an internal load balancer with a
self-signed certificate was unreachable over HTTPS.

Allow the CA to be supplied inline (caBundle, the chart renders the Secret) or
as a pre-seeded Secret (caSecretName), mounted and exposed as SSL_CERT_FILE.
That redirects only the system trust store, which nothing but the Vault client
consults — the database leg verifies against its own CA pool.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-08-25 18:11:39 +05:00
Alexey Artamonov 30af0e1f60 docs(postgres-operator): update values comment for the native 1.30.0 bump
The explanatory comment in values.yaml still described the removed
patches/cloudnative-pg-1.28.2.patch mechanism and the old 0.27.1/1.28.1
versions, and pointed at an "edit the patch" workflow that no longer exists.
Rewrite it to describe the native 0.29.0/1.30.0 chart bump (operator and CRDs
aligned by construction, no patch step) while preserving the still-valid
guidance: do not pin image.tag, keep the operator and CRDs in lockstep, since a
mismatched pin prunes CRD status fields and fails backups cluster-wide.

Addresses review feedback on #3951 (IvanHunters, CodeRabbit).

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-08-25 12:58:25 +03:00
IvanHunters 34e801cb9e fix(computeplane): stop tenant pool values leaking cluster-level keys and overflowing release names
The per-pool child HelmRelease spliced the tenant nodeGroup dict verbatim
(toYaml) after setting cluster/clusterReleaseName, and the NodeGroup schema
has no additionalProperties:false. A tenant could therefore (a) set a
duplicate cluster/clusterReleaseName key that wins the YAML merge and
re-parents the pool onto a tenant-controlled cluster, or (b) pass talos/
version/images through a shape-only knob and pick the worker OS image,
installer and kubelet version. omit those five keys at the splice so only the
pool's own shape flows through (tolerated extras like maxUnhealthy still do).

Separately, each pool renders a 38-char fixed release-name prefix against
Helm's 53-char cap, so a nodeGroup key over 15 chars created a HelmRelease the
apiserver accepts but Flux never reconciles, with no other signal. Guard the
length at render time, next to the ingress-role guard, mirroring the migration
overflow branch and the aggregated API's validateNameLength.

Also refresh the generated values doc: the module now materialises its own
default md0 and pool storageClass falls back to the kubernetes-nodes chart
default, not the wrapped kubernetes app.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-25 12:28:32 +03:00
Andrei Kvapil cdc34d6e34 fix(kubeovn-webhook): restrict the webhook port to the API server
The /mutate-pods handler returns namespace-derived annotations to any
caller that reaches it (GHSA-g883-q79m-8225), and until now nothing
stopped an arbitrary in-cluster pod from being that caller.

Client-certificate verification, added in #2698, cannot close this on
its own. Enabling --client-ca-file requires the API server to *present*
a certificate, which it does only when its AdmissionConfiguration names
a kubeConfigFile for the webhook plugins. Checked on a running
Cozystack control plane: the AdmissionConfiguration carries a
PodSecurity block and nothing else, so no certificate is offered at
all. Flipping to RequireAndVerifyClientCert there would reject the API
server itself, and with failurePolicy: Fail that stops pod creation
cluster-wide. No amount of log observation changes that — it is a
property of the control plane's configuration, not of the traffic.

So the network is where the caller can actually be constrained. The
policy denies pod and world traffic to the serving port and needs no
carve-out: the API server runs on the host network, so it is not a
Cilium endpoint and fromEndpoints never matches it. Unlike the
ingress-nginx admission webhook this component exists only on the
management cluster, so there is no konnectivity-tunnelled caller to
exempt either, and no forgeable pod label anywhere in the selector.

It is a CiliumNetworkPolicy rather than a core NetworkPolicy, and that
distinction is why it works at all: kube-ovn runs with ENABLE_NP: false
and programs no ACLs, so a core NetworkPolicy would be created and
never enforced. The values comment claiming NetworkPolicy was simply
not an option is corrected — it was true only of the core kind.

Deny-only with default-deny disabled, matching the ingress-nginx
admission policy: putting these pods behind a default-deny would mean
enumerating every port they serve and breaking traffic on the first one
forgotten.

Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-24 23:54:33 +02:00
Andrei Kvapil 4689b2b844 feat(kubeovn-webhook): add client-certificate verification, off by default (#2698)
## What this PR does

Adds client-certificate verification to the kube-ovn webhook, and turns
it on only when an operator supplies a CA bundle.

`--client-ca-file` is new and empty by default. With it empty the server
sets `tls.RequestClientCert`: it asks callers for a certificate, records
the issuer of whatever arrives, and serves the request either way. Set
the flag and the server switches to `tls.RequireAndVerifyClientCert`, so
a caller without a certificate signed by that CA is refused at the TLS
handshake.

**This is step one of two, and on its own it does not close
[GHSA-g883-q79m-8225](https://github.com/cozystack/cozystack/security/advisories/GHSA-g883-q79m-8225).**
With the default empty value nothing is rejected — the webhook still
answers an unauthenticated caller, and the only change is that the
caller's issuer now appears in the logs. That is deliberate: switching
straight to `RequireAndVerifyClientCert` would break every cluster whose
API server sends no client certificate, and there is no way to know from
here which those are. The logs are how we find out.

Step two, in a follow-up: read a day of issuer logs on a Talos cluster
and on `isp-full-generic`, confirm the API server presents a certificate
we can pin, then set the CA bundle so verification becomes mandatory.
The advisory should be published as fixed only after that lands — not
with this PR.

An earlier revision also carried a NetworkPolicy restricting ingress to
`kube-system`. It was dropped, and the title has been corrected to
match; only the TLS path is left here.

### Screenshots

N/A — no user-facing or 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: no row matches. The change
touches one system package and the Go image it builds — no app package,
no `values.schema.json`, no `ApplicationDefinition`, no platform
variant, no node prerequisite.

### Release note

```release-note
feat(kubeovn-webhook): the webhook now accepts --client-ca-file. Left empty (the default) it requests and logs client certificates without requiring them; set to a CA bundle it rejects callers that do not present a certificate signed by that CA.
```


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

* **New Features**
* Added optional client-certificate verification for the pod mutation
webhook through configurable CA settings.
* Added separate rate limits for requests with and without client
certificates.
* Added remote-address logging and clearer verification status
reporting.
* **Security**
* Requests continue to be forwarded while certificate observations are
recorded when verification is disabled.
* Serving-certificate updates remain supported; client CA changes
require a webhook restart.
* **Documentation**
* Documented client CA configuration and current certificate rotation
limitations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-24 23:35:31 +02:00
Alexey Artamonov 3c78c0be56 feat(clickhouse): add configurable version parameter
Add a `version` value (v25.8, v25.3, v24.9; default v24.9) to the
ClickHouse chart, backed by files/versions.yaml and resolved by
templates/_versions.tpl for both the ClickHouse server and Keeper
images, so the Cozystack API returns the engine version (spec.version)
like the other managed-DB charts. Default v24.9 -> 24.9.2.42 keeps
existing installations byte-for-byte identical.

The maintainer-run hack/update-versions.sh generator intersects the
server/keeper Docker Hub tags byte-collated (LC_ALL=C) so comm keeps
tags across the X.9 -> X.10 boundary; freezes the default major to the
tag it already ships (read from versions.yaml, so the freeze follows the
default); errors on a dropped default or an unresolvable major; writes
atomically; and runs on stock macOS bash 3.2 / BSD. Covered by
hack/clickhouse-update-versions_test.bats (10 cases) and the
tests/version_test.yaml helm-unittest suite.

Refs: #1246
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-08-24 23:41:27 +03:00
Alexey Artamonov b20e8ee91a fix(postgres-operator): bump CloudNativePG to 1.30.0 for the Cluster scale-subresource selector
Bump the vendored cloudnative-pg chart 0.27.1 -> 0.29.0 (operator + CRDs
1.28.1-patched-to-1.28.2 -> 1.30.0) and drop the hand-maintained
cloudnative-pg-1.28.2.patch entirely — 0.29.0 is a published chart, so no
version-alignment patch is needed.

Why this is required
--------------------
Database read-replica autoscaling (community proposal
design-proposals/database-horizontal-autoscaling) drives the CNPG Cluster
scale subresource with a stock HPA (via KEDA). That only works when the
subresource serves status.selector, which cloudnative-pg#8996 added and
shipped in CNPG 1.28.4 / 1.29.2 / 1.30.0. cozystack shipped 1.28.2, whose
scale subresource has no selector, so the HPA fails with InvalidSelector.

Why 1.30.0 and not 1.28.4
-------------------------
The floor is >= 1.28.4, but the upstream chart repo publishes no chart for
1.28.4 (the 1.28 line stops at chart 0.27.1 / appVersion 1.28.1), so 1.28.4
could only be reached by hand-patching the 0.27.1 chart's operator image and
CRDs up to the backport. A live PoC on a dev cluster proved that path is
broken: the 1.28.4 operator running under the 0.27.1 (1.28.1) chart
templates crash-loops on a missing RBAC grant it needs but the older chart
does not provide —

  endpointslices.discovery.k8s.io is forbidden: ... cannot list resource
  "endpointslices"

— so status.selector is never populated and the HPA still cannot scale. The
hand-patch would have to port the operator's RBAC (and any other template
delta) from 1.28.1 to 1.28.4 by hand, with no published chart to diff
against. 1.30.0 (chart 0.29.0) is the only published chart that carries the
selector together with matching RBAC/CRDs/templates, and the 1.28 line is
EOL (2026-06-30), so 1.30.0 also keeps us on a supported release. cozystack
uses the barman-cloud plugin (not native barman, removed in 1.31) and no
CNPG Poolers, so the 1.30 deprecations and the known pooler bug do not
apply.

Update the version-guard unittest to 1.30.0 and assert the clusters CRD
scale subresource carries labelSelectorPath: .status.selector.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-08-24 22:46:57 +03:00
Andrei Kvapil 724354ffe1 fix(console): address the review findings on the Admin portal rework
Four of the five findings share one root cause: a node in the tenant
tree can attach to its nearest *visible* ancestor when its real parent
is inaccessible, and per-row actions then derived their target from
that bridged ancestor.

The Edit action on a bridged tenant row pointed at a Tenant CR that
does not exist. The CR lives in the real parent's namespace, so the
target now comes from realParentNamespace — and Edit is offered only
when that namespace is readable, because otherwise there is no CR the
user could open and the button could only ever dead-end. The test that
asserted two Edit buttons had frozen the broken target as expected
behaviour; it now names the rows it expects, and the Edit button gained
a title so that assertion means something.

An inherited module chip is rendered only when the providing tenant is
NOT visible, yet it was a live button: clicking it denied the instance
GET and switched the active tenant to a namespace outside the visible
set, which the selector then bounced to an arbitrary fallback. It is
now an inert badge that states where the module comes from.

InfoRedirect's missing-definition branch hardcoded /console, throwing an
admin out of the portal its own docstring promises to preserve.

The sidebar's alsoMatch used a bare startsWith, so /admin/apps would
light up for /admin/apps-v2. It now matches whole path segments.

And SecretsTab let a rejected clipboard write escape as an unhandled
rejection, leaving the button silent — a denied permission or an
insecure context was indistinguishable from a no-op. It now reports the
failure.

Declined: CodeRabbit asked to split `import { useK8sList, type K8sResource }`
into a separate `import type`. The inline modifier is valid under
verbatimModuleSyntax and is what 26 other imports in this console use,
so the change would make the file the odd one out.

Reported-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-24 10:45:20 +02:00
Andrei Kvapil 8554c036de fix(ingress-nginx): confine the konnectivity carve-out to kube-system
The admission-webhook deny exempted any endpoint carrying
k8s-app: konnectivity-agent, in any namespace. Because the rule
disables default-deny, falling out of the deny is the entire access
decision — so any pod anywhere could reach the webhook port by
labelling itself, which is precisely the access the policy exists to
remove.

k8s-app is an ordinary pod label a workload sets on itself. Nothing in
the platform constrains pod labels by admission, and Cilium keeps
k8s-app identity-relevant (its label filter ships no default
whitelist), so the forged label really does change the endpoint's
identity rather than being filtered out.

The namespace key is derived by Cilium from the pod's actual namespace
and cannot be set by the workload. Splitting the deny in two keeps the
carve-out and closes the hole: one selector for everything outside
kube-system with no label condition at all, one for kube-system
carrying the existing NotIn. Only the agent in its own namespace is
exempt.

Kamaji at the pinned 26.3.6-edge puts nothing but
k8s-app: konnectivity-agent on the agent pod template
(internal/resources/konnectivity/agent.go), so the label remains the
only available discriminator and the namespace confinement is what
makes it safe to key on.

Tests cover both halves: the outside-kube-system selector must carry no
label condition, and the kube-system selector must keep exactly the
namespace and label pair.

Reported-by: Daniil Myasnikov <myasnikovdaniil2001@gmail.com>
Assisted-By: Claude
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-21 14:41:27 +02:00
Andrei Kvapil bfd4ea0643 feat(dashboard): land on the Console instead of the Marketplace
Opening the dashboard without a path sent the user to the catalog, and
the header gave Marketplace the primary emphasis. The Console is the
better entry point: it shows what the tenant already runs, which is
what someone opening the dashboard almost always came for. The
Marketplace stays one click away for when the intent is to add
something new.

The landing path becomes a named constant beside the portal helper, so
the redirect and the tab emphasis cannot drift apart, and the library's
own default tab set is reordered to match rather than contradict its
only consumer.

Assisted-By: Claude
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-21 10:27:40 +02:00
Andrei Kvapil cfd5444f16 fix(dashboard): let a masked secret be copied and confirm the copy
Copying a secret key required revealing it on screen first, and the
button gave no sign that anything reached the clipboard. The value the
list already carries is enough to copy, so the button works while the
key stays masked, and it briefly turns into a check mark to confirm.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-20 21:16:13 +02:00
Andrei Kvapil e75500682c fix(dashboard): tie navigation to where a page was opened from
Detail pages offered a fixed tab set, so Info showed Workloads, Services
and Ingresses tabs it never fills; tabs now appear only for resource
groups the instance actually owns. The probes reuse the tabs' own list
refs and label selector, so they share one cache entry and one watch.

Back and the sidebar followed the plural instead of the entry point: a
module page sent the user to a one-item list and lit nothing in the
sidebar. Module pages now return to Modules and keep it highlighted,
while Info returns to Tenants and highlights that, matching the trees
they are reached from.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-20 21:15:57 +02:00
Andrei Kvapil 1279652b56 feat(dashboard): list External IPs across every accessible tenant
The page showed LoadBalancer services of the selected tenant only, so an
operator had to switch tenants one by one to find an address. It now
lists them for every visible TenantNamespace in a single table with a
Namespace column. A namespace whose services the user cannot list
contributes no rows instead of failing the page.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-20 21:15:39 +02:00
Andrei Kvapil 21b8a0e2f0 feat(dashboard): rebuild Modules as a tenant tree of module chips
The Modules page listed the selected tenant's modules as Enabled or
Disabled cards, so a module a parent tenant provides looked disabled and
clicking it led to a Tenant CR that does not exist in the child's
namespace — an endless spinner.

It now covers every tenant the user can access, as the same hierarchy
tree the Tenants page draws, with each tenant's modules as chips
carrying the module icon, the namespace running it and its readiness.
Availability comes from the `namespace.cozystack.io/<module>` label on
each TenantNamespace, which names the providing namespace: the tenant
itself for a local module, an ancestor for an inherited one. An
inherited chip appears only when that ancestor is out of the user's
reach, since otherwise its own chip already shows the module. Clicking
any chip opens the module in whichever tenant runs it.

Tenants whose whole subtree has no modules collapse into a
"+ N tenants" branch, and the Info module is left out entirely as every
tenant carries it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-20 21:15:26 +02:00
Andrei Kvapil 7a48d455ac feat(dashboard): show the tenant hierarchy on the Tenants page
The flat table becomes a tree built from TenantNamespaces, whose
`tenant.cozystack.io/<ancestor-ns>` labels carry the full ancestry. A
tenant whose parent is not accessible attaches to its nearest visible
ancestor instead of disappearing, and rows are labelled relative to that
parent, so a sub-tenant reads as `crpjxhwm` rather than
`whmcs-crpjxhwm`. Subtrees collapse from the row itself.

Each row also carries its own actions: Info opens that tenant's Info
page, and Create Tenant orders a sub-tenant at any level of the tree.
Editing a tenant switches to the namespace holding its Tenant CR — the
parent's, or its own for the hierarchy root.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-20 21:15:07 +02:00
Andrei Kvapil ef73a0e47c feat(dashboard): move Administration into the Admin portal
Administration (Tenants, Modules, External IPs, per-tenant Info) leaves
the Console sidebar for Admin, which is now always visible: those pages
need no special permission. The two cluster-wide operator areas keep
their own gates, so Capacity and Backup Classes still appear only for
users who can open them.

Resource routes become portal-aware through a shared base-path helper,
so a detail, edit or order page opened from Admin stays under /admin
instead of jumping back to /console.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-20 21:14:50 +02:00
Andrei Kvapil 5178e169b1 feat(dashboard): drop the Marketplace landing page
Open the grouped application list right away instead of the
"Choose a product to deploy" page and its Show-all-apps button.
Category links keep filtering to a single category.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-08-20 21:14:33 +02:00