Commit Graph
4502 Commits
Author SHA1 Message Date
Aleksei Sviridkin d76bd1f263 fix(mariadb): mount the chart's my.cnf under a .cnf name
mariadb-operator up to v0.0.27 mounted the referenced ConfigMap key as
/etc/mysql/conf.d/my.cnf whatever the key was called. Since v0.0.28 the
file takes the key's own name, so the chart's key "config" became
/etc/mysql/conf.d/config, and the server's !includedir reads only *.cnf
files from that directory. Every setting in the chart's my.cnf has been
ignored since that operator bump: instances run on stock defaults,
including a 128M InnoDB buffer pool inside the 128Mi default limit.

Renaming the key restores the file as intended. Existing instances roll
their pods once, since the projected volume in the pod template changes,
and come back with the chart's settings in effect, including sql-mode
and innodb_flush_log_at_trx_commit, not only the resource sizing.

Fixes: a9c42c8ef0 ("Update mariadb-operator v0.28.1 (#124)")
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-10 02:35:48 +03:00
Aleksei Sviridkin f61e13fdef fix(cilium): size the agent memory limit for its BPF maps (#4803)
## What this PR does

This raises the cilium-agent memory limit from 4Gi to 24Gi. The 4Gi
limit from #3495 has no room for the agent's BPF maps. On nodes with
roughly a terabyte of RAM or more, the agent is OOM-killed on every
start after a reboot, and the node loses pod networking.

The maps grow with node RAM. Cilium sizes its CT, NAT, neighbor and
socket rev-NAT maps as a share of total RAM
(`bpf-map-dynamic-size-ratio`, 0.0025 by default), and caps each map at
16Mi entries. These are LRU hash maps, so the kernel preallocates them
in full. Since Linux 5.11 that memory is charged to the memcg of the
process that creates the maps, which is the agent container.

This is also why the upgrade itself works. A map reused from bpffs stays
charged to the cgroup that created it. The failure shows up on the first
reboot after the upgrade, when the maps are recreated inside the limited
cgroup.

Policy maps don't belong on that list. The issue and the upstream
chart's configmap comment both say the ratio sizes them, but at this
version they are LPM tries sized by `bpf.policyMapMax`, and they
allocate per entry.

One DaemonSet serves every node, so the limit can't follow node RAM. The
map sizes are capped, though, so a fixed limit can cover any node. With
every map at 16Mi entries for both IPv4 and IPv6, the maps take just
under 20Gi of kernel memory. Add the 4Gi the agent already had and you
get 24Gi. The request stays at 256Mi.

The limit doesn't reserve memory, and the pod cgroup still gets
`memory.max`, which is what #3495 was after. What it costs is the leak
backstop. The agent is now stopped at 24Gi minus its own map memory, so
on most nodes a leak in its heap is no longer caught before the node
feels it. If someone prefers a tighter backstop over IPv6 headroom, they
can lower `cilium.resources.limits.memory` in the networking component
values. About 13Gi still covers the IPv4-only map ceiling.

I didn't pin the map sizes (`bpf.ctTcpMax`, `bpf.natMax` and so on). No
single value works for every node. A value small enough for a 16Gi node
cuts the conntrack capacity of a 1.5TiB node many times over. A pinned
size also differs from what almost every node above ~14Gi of RAM runs
today, so the agent would recreate those maps on upgrade and drop their
conntrack and NAT state. A lower ratio has the same problem and doesn't
lower the ceiling. The new test keeps the ratio at its default and the
sizes unpinned, so changing either has to be deliberate.

The figures are computed, not measured. I read the sizing code at cilium
1.19.5 (`pkg/option/config.go`), and 1.20.2, which #3343 moves to, has
the same code. Memory comes from that formula plus the kernel hash map
layout: a 48-byte element header, the key and value rounded up to 8
bytes, and a 16-byte bucket per power of two of `max_entries`. By that
math the IPv4 maps alone pass 4Gi at roughly a terabyte of RAM. Bucket
rounding moves the exact point, so I wouldn't read more precision into
it. For the 1.5TiB node in the report, the issue's 3.9GiB is the agent's
own budget (0.25% of RAM), and the kernel's actual charge comes out
higher.

Follow-ups, not in this PR:

- Operators who want smaller maps on large nodes can size them per node
with a `CiliumNodeConfig` selected by node label. This needs a
documentation note.
- The reporter's `bpftool` measurement of the real map memory is still
pending. That is the measured check for the numbers above.

The cilium helm-unittest suite passes on this branch, and also with this
change applied over the 1.20.2 chart from #3343. Each new assertion
fails when its value changes: the limit, the CT, NAT and neighbor sizes,
the socket rev-NAT size set through `extraConfig`, and the ratio.

Closes #4800

### Screenshots

Not a UI change.

### Downstream repositories

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

The diff changes only `packages/system/cilium/values.yaml` and its test.
None of the trigger-map entries apply: no app, schema, platform value,
variant, release asset or tooling changes, and no website page documents
the agent limit.

### Release note

```release-note
fix(cilium): raise the cilium-agent memory limit from 4Gi to 24Gi so it covers the agent's BPF maps, which cilium sizes from node RAM. On nodes with around a terabyte of RAM or more, the 4Gi limit got the agent OOM-killed on every start after a reboot, leaving the node without pod networking.
```


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

## Summary by CodeRabbit

* **Bug Fixes**
  * Increased the Cilium agent memory limit to 24 GiB.
* Updated validation for BPF map sizing: checks the dynamic size ratio
and confirms that specific map-size settings are absent.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-09 20:50:44 +03:00
Aleksei Sviridkin aa804aaa83 fix(dashboard): allow editing free-form objects in application forms (#4682)
## What this PR does

Free-form object fields, such as NATS `config.merge` and
`config.resolver`, get a JSON editor. The form keeps nested objects,
arrays and primitive values. Input that is not a JSON object stays
visible, and schema validation blocks the create or update request until
it is fixed. A JSON object that fails the schema marks the field invalid
and points it at the error list. Typed maps keep their key and value
editor, and read-only and immutable fields stay protected.

Fixes #4676.

### Validation

- All 463 console tests in 63 files pass, and so does the TypeScript
check.
- Tests cover typing into the editor, nested NATS objects next to the
typed users editor, input that is not a JSON object, schema errors on a
valid object, existing values, immutable fields and free-form objects
inside arrays. On the order page they cover a create after a form and
YAML round trip, saving an edited object, and blocking input that is not
JSON in create and edit mode.
- The two schema-error tests fail on the component without the change.

### Screenshots

![Nested
JSON](https://github.com/user-attachments/assets/04c95513-4e7d-465a-9c7d-e4384e13a47c)

![Invalid
JSON](https://github.com/user-attachments/assets/8cbb0e88-965a-4765-b0f9-9c0ab8c7f065)

### Downstream repositories

This change preserves the public resource schema and client contract.
The file-level trigger map was reviewed; no companion repository update
is required.

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

### Release note

```release-note
fix(dashboard): allow editing free-form objects in application forms
```


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

* **New Features**
* Added a JSON editor for configuration fields that accept objects with
arbitrary properties. Edit nested values directly, with changes retained
when switching between form and YAML views.
* **Bug Fixes**
* Invalid JSON is flagged and prevents create or update submissions
until corrected. The editor supports empty objects, read-only fields,
and objects nested in arrays, and makes validation errors accessible.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-09 02:21:30 +03:00
Aleksei Sviridkin 6f49e5f427 feat(openbao): optional static-key auto-unseal and apiserver egress label (#4766)
## What this PR does

This continues #4168 by @txmazing, which has been idle since 2026-09-10.
The first commit is the work from #4168 squashed into one, with
@txmazing as author. The other two commits constrain the new values and
complete the documentation.

Five changes to the managed OpenBAO app (`packages/apps/openbao`).

1. **Optional static-key auto-unseal** (#2787, #2483). `seal.type` is
`shamir`, the unchanged default, or `static`. With `static` the chart
mounts the Secret named in `seal.secretName` through the upstream
chart's `server.extraVolumes` and adds OpenBAO's `seal "static"` stanza
to the standalone and the Raft config, so the pods unseal themselves
after a restart. `seal.previousSecretName` and `seal.previousKeyId`
carry the n-1 key during a rotation. The chart never generates the key.
A cluster administrator creates the Secret in the tenant namespace,
because the `cozy:tenant:*` roles grant nothing on Secrets. A way for
tenants to supply secrets by reference is proposed separately in
cozystack/community#82.
2. **No silent seal change.** The template reads the seal in use from
the `<release>-config` ConfigMap and refuses a different `seal.type`
unless `seal.allowMigration: true` acknowledges a migration. Going from
`static` back to `shamir` is refused, because the chart cannot render
the disabled stanza that direction needs.
3. **Apiserver egress label** (#2793). The server pods carry
`policy.cozystack.io/allow-to-apiserver: "true"`, so
`service_registration "kubernetes"` reaches the API server and HA pods
no longer hang at start.
4. **Constrained values.** The upstream chart passes the server config
through `tpl`, and its start script replaces upper case placeholders
such as `HOSTNAME` in it. A key identifier with template delimiters
would run as a template inside helm-controller, which runs as
cluster-admin, so `seal.keyId` and `seal.previousKeyId` take lowercase
alphanumerics, hyphens, dots and underscores. The Secret names become
pod volume names `userconfig-<name>`, so they take DNS labels of at most
52 characters. A previous Secret or key identifier equal to the current
one fails at render. The apps API only defaults against the schema, and
Helm enforces these patterns at install and upgrade.
5. **README.** The trade-off of `static`, the key Secret, the rotation
with the pod replacement that the `OnDelete` strategy requires, the
migration from Shamir step by step after the OpenBAO guide, and the
one-time pod replacement after an upgrade so that existing pods get the
label.

### Validation

- `helm unittest .` in `packages/apps/openbao` passes 28 of 28 with
helm-unittest 1.1.2, the version CI pins, and with 1.2.1.
- Dropping the key identifier patterns, moving the Secret name limit to
51 or 53, or removing either equality guard turns the suite red each
time.
- Rendering this chart and then the vendored `openbao` chart with a
template in `seal.keyId` now stops at the schema. Without the pattern
the same two-stage render executed it, `lookup` included.
- With default values, standalone and with `replicas: 3`, `helm
template` renders the same as `main` apart from the six-line
`extraLabels` block.
- `make generate` in the package with cozyvalues-gen v1.7.0 and at the
repository root with Go 1.27.1 leaves the tree unchanged. `make
unit-tests` passes apart from seven BATS tests that fail the same way on
a clean `main` under macOS, most of them because macOS ships no
`timeout`. In `make test-controllers` only `internal/backupcontroller`
fails, because its tests do not build on current `main` either, which
#4768 fixes. `hack/check-commit-trailers.sh` passes and `cmd/api-gate`
reports no sizeable API change.
- Nothing here was run on a cluster. #4168 reports a Cozystack v1.6.3
cluster where a `replicas: 1` instance with `seal.type: static` came
back `Sealed false` after `kubectl delete pod`.

TLS between pods and an init or bootstrap job are not covered. Both are
on the #2787 list and independent of this change.

### Screenshots

No dashboard code change. The application form gains the `seal` group
from the regenerated `openAPISchema` and `keysOrder`.

### Downstream repositories

Walked the trigger map against the diff. The new `seal` field in
`packages/apps/openbao/values.schema.json` reaches the hand-written
`cozystack_openbao` resource of the Terraform provider. That is tracked
in cozystack/terraform-provider-cozystack#44, which also needs
`allowMigration` and the value constraints. The website reference page
is generated from this `README.md` for the apps listed in its
`Makefile`, and `openbao` is in `APPS`, so the page follows the next
stable tag without a follow-up. The website pages that mention the
apiserver label describe it in general terms and carry no OpenBAO
workaround. There is no `hack/` change, no platform values change, no
variant and no change to ApplicationDefinition semantics.

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

### Release note

```release-note
feat(openbao): the OpenBAO app can auto-unseal with OpenBAO's static-key seal (`seal.type: static`) from a Secret that a cluster administrator creates in the tenant namespace, with n-1 rotation through `seal.previousSecretName` and `seal.previousKeyId`. Shamir stays the default, and changing the seal of an existing instance needs `seal.allowMigration`. Server pods now carry `policy.cozystack.io/allow-to-apiserver: "true"`, so HA instances no longer hang at start behind the apiserver egress policy.
```


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

## Summary by CodeRabbit

* **New Features**
* OpenBAO supports static sealing with keys stored in an existing
Secret; Shamir sealing remains the default.
* Configure current and previous keys to support key rotation in
standalone and high-availability deployments. Keep the previous key
until stored keys have been re-wrapped.
* Seal-type changes are validated. Shamir-to-static migration requires
explicit acknowledgement; static-to-Shamir migration is not supported.
* Static-seal settings validate required key details and Secret names.
Server pods receive a label needed by the configured API-server egress
policy.
* **Documentation**
* Added guidance for configuring static sealing, managing Secrets,
rotating keys, and migrating seal types.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-09 02:17:21 +03:00
Aleksei Sviridkin 3dc0d046b3 fix(postgres): cap the default shared_buffers at a quarter of the memory limit (#4724)
## What this PR does

The chart renders no `shared_buffers`, and CloudNativePG does not set
one either (its default parameters in `pkg/postgres/configuration.go` at
v1.30.0, the operator version this repo ships, have no such key).
PostgreSQL therefore starts with its built-in 128MB whatever the
container gets. A full buffer pool is the whole memory limit on
`t1.nano` and half of it on `t1.micro`, which is the chart's default
preset. The pool is shared memory that the kernel cannot reclaim without
swap. A small or idle database never fills it, but a dump, index scans
over a large table or a replica catching up after a failover can, and
then the instance is OOMKilled.

Observed on a cluster running Cozystack 1.4.3, whose chart renders the
same parameters, with a three-instance `t1.nano` database. A
`pg_dumpall` run through `kubectl exec` inside the primary's container
got that container OOMKilled. The dump client ran in the same container
and shared its limit, so that first kill is weak evidence on its own.
After the failover the old primaries could not rejoin. Their `pg_rewind`
against the new 128Mi primary kept failing with `could not fetch file
list: SSL SYSCALL error: EOF detected` until that primary's memory limit
was raised to 512Mi, and then both rewinds completed.

Below a 512Mi memory limit the chart defaults `shared_buffers` to a
quarter of the limit with a 16MB floor, capped at the rendered memory
request rounded down to whole MiB. CloudNativePG rejects a Cluster when
`shared_buffers` exceeds that request, which can happen with memory
overcommit. That is 32MB on `t1.nano` and 64MB on `t1.micro`. From 512Mi
up a quarter is at least 128MB, so nothing is rendered and those
clusters keep the PostgreSQL default exactly as before. An explicit
`shared_buffers` in `postgresql.parameters` still wins at any size. The
limit is read through `cozy-lib.resources.defaultingSanitize`, the
helper that fills `spec.resources`, which is also how
`packages/apps/mariadb/templates/_tuning.tpl` sizes the InnoDB buffer
pool. Presets and partial `resources` overrides therefore resolve
exactly as they do for the container.

A quarter is the starting point the PostgreSQL documentation gives for a
dedicated server with 1GB or more, and it advises a smaller share below
1GB, so this is a ceiling for small instances rather than tuning. Larger
presets are left alone on purpose. Deriving a bigger pool for them is a
tuning change that would restart every PostgreSQL instance on upgrade,
and it does not belong in a fix.

### Upgrade impact

`shared_buffers` needs a restart. On upgrade every cluster whose memory
limit is below 512Mi and that does not set `shared_buffers` gets the new
value, and that includes every cluster created with the default preset.
The chart leaves `primaryUpdateStrategy` and `primaryUpdateMethod` at
their CloudNativePG defaults, `unsupervised` and `restart`, so the
operator restarts the replicas first and then the primary in place, and
writes pause for the length of one PostgreSQL restart. A single-instance
cluster is unavailable for that time. Clusters from 512Mi up render the
same manifest as before and are not touched.

### Validation

- `helm unittest` passes all 166 tests in 21 suites. The new suite
covers the default preset, `t1.nano`, a `resources` override that sets
only cpu, a 500Mi limit just under the threshold, the 16MB floor,
`t1.small` and `t1.large` rendering no key, an explicit value on a small
and on a large preset, and the other default parameters. Seven
additional cases cover memory overcommit, rounding down a fractional MiB
request, a request below the 16MB floor, explicit values and the
unchanged 512Mi threshold. Four cases fail against the previous PR
template.
- Breaking the threshold comparison, the 16MB floor or the divisor, or
dropping the new block, turns at least one case of the suite red each
time.
- `helm template` of the whole chart renders `t1.small` exactly as the
base does, and the default preset differs only by `shared_buffers:
"64MB"`.
- `make generate` with cozyvalues-gen v1.7.0, the version CI pins,
leaves `README.md`, `values.schema.json`, the Go types and the
ApplicationDefinition unchanged, because `values.yaml` only gains a
comment.
- The new default was not exercised on a live cluster.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff. It touches one chart template, a comment in `values.yaml` and a
new test file. No package is added, renamed or removed, no field in
`values.schema.json` is added, removed or renamed, and no default in
`values.yaml` changes, so the Terraform provider has nothing to restate
and the generated README is identical. Neither the website nor the
provider mentions `shared_buffers`. No entry 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:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
fix(postgres): default shared_buffers to a quarter of the memory limit with a 16MB floor, capped at the memory request, when the limit is below 512Mi and shared_buffers is not set. PostgreSQL's built-in 128MB was the whole memory limit on t1.nano and half of it on t1.micro, the default preset, so a dump, index scans over a large table or a replica resync could fill the buffer pool and get the instance OOMKilled. Affected clusters restart their instances once on upgrade, the primary last, with a short pause in writes. Clusters from 512Mi up and clusters that set shared_buffers explicitly are unchanged.
```


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

## Summary by CodeRabbit

* **New Features**
* PostgreSQL now automatically adjusts its default memory allocation for
deployments with memory limits below 512 MiB, while retaining
PostgreSQL’s default for larger limits. Explicitly configured values
continue to take precedence.
* **Documentation**
* Clarified how the automatic memory allocation works, including its
minimum and memory-request cap.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-09 00:10:40 +03:00
Yan Bondarenko 4b606036bb fix(mongodb): set the s3 bucket and prefix on the bootstrap restore
With bootstrap.enabled the chart rendered a PerconaServerMongoDBRestore
without backupSource.s3.bucket. The operator CRD requires that field, so
the API server rejected the restore and the HelmRelease failed. The prefix
was missing too, and it matters just as much. For a backupSource restore
the operator builds the PBM storage from backupSource.s3 alone and takes
only the backup name from backupSource.destination, so a restore without
the prefix would look for the backup at the bucket root.

The restore now takes the bucket and prefix from the same helpers that
fill the backup storage, so it reads from where the backups are written.
Both places quote the two values, so a numeric bucket or prefix stays a
string instead of making the resource invalid. The backup storage still
leaves out an empty prefix, so running releases keep the same object. A
destinationPath that names no bucket fails at render time instead of
reaching the operator with an empty bucket.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-10-08 22:39:14 +03:00
Yan Bondarenko 3ea7a95108 docs(openbao): document the static seal trade-off and the Shamir migration
With the static seal the key sits in a Secret in the same cluster as the
data it unwraps, so the barrier is only as strong as access to Secrets
in that namespace, to etcd and to backups. The README now says so before
the reader picks a seal, and it says what the key identifier is for,
since OpenBAO picks the key for stored data by it.

Tenant roles grant nothing on Secrets and no pods/log, so a cluster
administrator creates the key Secret and reads the log line that
confirms a re-wrap.

The StatefulSet uses the OnDelete strategy, so neither a seal migration
nor the apiserver egress label reaches a running pod until it is
replaced. The README describes the migration from Shamir step by step,
following the OpenBAO seal migration guide, and says that pods created
before the label have to be replaced once.

The migration starts with a backup. The standalone file backend is not
transactional, so its volume is copied with the server stopped or
snapshotted atomically. The upgrade that sets allowMigration is the
point of no return for the chart, and the README says how a cluster
administrator backs out of it before any pod is replaced.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-10-08 22:39:10 +03:00
Yan Bondarenko bfbb5ac75f fix(openbao): constrain the seal values that reach the server config
The upstream chart passes the server config through tpl, and its start
script replaces upper case placeholders such as HOSTNAME anywhere in the
copied file. A key identifier with template delimiters would run as a
template inside helm-controller, which runs as cluster-admin, and an
upper case one could come out different on every pod. Both key
identifiers now take lowercase alphanumerics, hyphens, dots and
underscores.

The Secret names become pod volume names of the form userconfig-<name>,
which Kubernetes validates as DNS labels. Both name fields now take a
DNS label of at most 52 characters, and the volume entries are quoted.
The apps API only defaults against the generated schema, so Helm
enforces these patterns when it installs or upgrades the release.

A previous Secret equal to the current one mounts the same volume twice,
which the API server rejects for the StatefulSet, and OpenBAO refuses to
start when equal key identifiers name different key material. Both
cases now fail at render. The deepcopy functions for the Seal type are
regenerated as well.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-10-08 22:39:10 +03:00
txmazing 0c52dedbe2 feat(openbao): optional static-key auto-unseal and apiserver egress label
The OpenBAO app has no auto-unseal (#2787). Every restart of a server,
after a node drain, an image bump or an OOM kill, leaves it sealed until
an operator enters the Shamir key shares into each replica. In HA mode
the pods also hang at start (#2793). Cozystack's allow-to-apiserver
policy only admits egress from pods labelled
policy.cozystack.io/allow-to-apiserver, the app never set that label,
and service_registration "kubernetes" cannot reach the API server
without it.

A new seal value selects shamir, which stays the default, or OpenBAO's
seal "static". With static the chart renders the static seal stanza into
the standalone and the Raft config and mounts the key Secret named in
seal.secretName through the upstream chart's server.extraVolumes. The
chart never generates, stores or rotates the key, because a missed
lookup would replace it and leave the data unreadable.
seal.previousSecretName and seal.previousKeyId carry the n-1 key that
the static seal accepts during a rotation. The server pods always carry
the apiserver egress label.

Changing the seal of an initialized instance is a seal migration that
OpenBAO performs under bao operator unseal -migrate. The chart reads the
seal in use from the config ConfigMap the previous release rendered and
refuses a different seal.type unless seal.allowMigration acknowledges
the migration. Moving away from static needs the old stanza kept with
disabled = "true", which the chart does not render, so that direction is
refused regardless. The README documents the key Secret, the rotation
with the pod replacement that the OnDelete update strategy requires, and
the log line that confirms the re-wrap.

Signed-off-by: txmazing <txmazing@gmail.com>
Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-10-08 22:39:09 +03:00
Yan Bondarenko 9c0085c358 fix(postgres): cap shared_buffers at the memory request
CloudNativePG rejects clusters whose shared_buffers exceeds the memory
request. Memory overcommit can put that request below both a quarter of
the limit and the 16MB floor.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-10-08 22:39:05 +03:00
Yan Bondarenko 6dbd76ae50 fix(postgres): cap the default shared_buffers at a quarter of the memory limit
The chart renders no shared_buffers, so PostgreSQL starts with its 128MB
default whatever the container gets. A full pool is the whole memory
limit on t1.nano and half of it on t1.micro, the default preset. The
pool is shared memory that the kernel cannot reclaim without swap, so
when a dump, index scans over a large table or a replica catching up
after a failover fill it, the instance is OOMKilled.

Instances below a 512Mi memory limit now default to a quarter of the
limit, at least 16MB. From 512Mi up a quarter is at least 128MB, so
those instances render exactly as before. shared_buffers needs a
restart, so on upgrade CloudNativePG restarts every instance below
512Mi that does not set it explicitly.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-10-08 22:39:05 +03:00
Yan Bondarenko 96b35893a5 fix(dashboard): mark a free-form field invalid when the schema rejects it
A JSON object that parses but fails the schema, for example on
minProperties, left the textarea with aria-invalid false, and nothing
linked it to the error list RJSF renders under the field, so assistive
technology announced the field as valid. The field now reports the
schema errors RJSF passes in and points aria-describedby at their list,
unless hideError keeps that list from rendering. The red help text
stays tied to input that is not a JSON object.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-10-08 22:39:01 +03:00
Yan Bondarenko d639113caf fix(dashboard): allow editing free-form objects in application forms
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
Assisted-by: LLM
2026-10-08 22:39:01 +03:00
Aleksei Sviridkin 87a64071a7 fix(cilium)!: take cilium's own addresses from a block kube-ovn does not hand out (#4778)
## What this PR does

fixes #3740.

on the kube-ovn variants cilium allocates no pod ips, but the agent
still takes its router ip (`cilium_host`, `CiliumInternalIP`) and the
gateway api ingress ip from its own allocation cidr. with `ipam.mode:
kubernetes` that's `Node.spec.podCIDR`, a /24 out of the same /16
kube-ovn hands pods from, so sooner or later kube-ovn gives a pod an
address cilium already holds. we hit it on three clusters, details in
https://github.com/cozystack/cozystack/issues/3740#issuecomment-6022394243.

**what changes**

- the kube-ovn variants run cilium with `ipam.mode: cluster-pool`, a /29
per node out of the last /21 of `networking.podCIDR` (`10.244.248.0/21`
for the default /16). the platform derives the block from
`networking.podCIDR` and passes the same range to kube-ovn as
`--default-exclude-ips`, so a fresh install never hands it to pods.
- the block sits inside the pod cidr on purpose. envoy reaches gateway
api backends from the node's ingress ip, and kube-ovn masquerades host
originated traffic only when the source is in `ovn40subnets`. with the
`100.65.0.0/16` pool from the issue a nodeport through the gateway
answered 200 only on the backend's node and 503 `upstream connect error
... connection timeout` on the other two. with the block inside the pod
cidr it's 200 from all three. worth a look on the ci side too. the ci
sandbox sets talos `podSubnets` to `100.65.0.0/16`, so cilium's ingress
ips there already come from outside `ovn40subnets`, and the gateway e2e
checks Accepted, Programmed and the lb service but sends no request
through a listener, so it wouldn't notice. with this pr the sandbox
takes its block from the pod cidr like everyone else.
- the end of the /16 is what kube-ovn reaches last. it hands out the
lowest free address and reuses released ones only when the free range
runs dry (`pkg/ipam/subnet.go` and `pkg/ipam/ip_range_list.go` at
v1.15.26). a /21 cut into /29s covers 256 nodes.
- the slim variants and guest clusters stay on `ipam.mode: kubernetes`.
cilium is the only cni there and `Node.spec.podCIDR` is the pod range.
- `networking.podCIDR` now needs /20 or wider on the kube-ovn variants,
otherwise the render fails and says why. an install that rendered before
can stop rendering at upgrade, hence the `!`. a /21 cut into /29s also
caps these variants at 256 nodes, a fresh install included.

**existing clusters**

a post-upgrade hook in the cilium chart moves them
(`templates/ipam-migration.yaml`, `files/ipam-migration.sh`). before it
touches anything it checks that the block has a /29 for every CiliumNode
and that it lies inside kube-ovn's default subnet, and stops with both
cidrs printed otherwise. a node cleared past the pool's capacity would
lose pod networking on its next agent restart, and a block outside the
subnet puts the agents outside `ovn40subnets` again. it has to be
post-upgrade. platform migrations run as the platform's pre-upgrade
hook, while every agent still runs `ipam.mode: kubernetes` and writes
`Node.spec.podCIDR` back into a cleared CiliumNode
(`pkg/nodediscovery/nodediscovery.go` at v1.19.5), so the move can only
happen after the cilium release switched the mode. the job runs on the
host network against the agent's own api endpoint (kubeprism on talos),
because it restarts the agents.

1. adds the block and every node's current ingress ip to `excludeIps` of
the default subnet, in one json patch with a `test` op, and reads the
subnet again on conflict, since kube-ovn rewrites that list into its own
order right after a patch. the old ingress ip goes in because cilium
1.19.5 keeps a former ingress ip in the agent's ipcache as
`reserved:ingress` after the move. we saw 2 to 3 such entries per node
hours later. old router ips leave the ipcache and go back to kube-ovn.
2. stops right there if an address in the block is still allocated,
names it, and moves no agent. it first checks the subnet status it
patched against, then waits until `status.v4availableIPrange` stops
offering the block, which is kube-ovn having applied the exclusion, and
then reads the `ips.kubeovn.io` objects of the default subnet. the
status alone can't answer it, kube-ovn leaves excluded addresses out of
`v4usingIPrange` (`GetSubnetIPRangeString` in `pkg/ipam/ipam.go` at
v1.15.26), so a pod still in the block would be invisible on a retry
where the exclusion is already in place. recreate those pods and the
next upgrade retry goes through.
3. one node at a time, clears the inherited /24 from
`spec.ipam.podCIDRs` (again with a `test` op, so a concurrent writer
fails the patch instead of losing an update), waits for the operator to
allocate a /29 from the block and restarts that node's agent. a node
that isn't ready is skipped and its agent picks up the block when it
starts. nodes already moved are left alone, so a retry after a timeout
just continues. the hook runs inside the cilium upgrade, so where
restarting every agent takes longer than the upgrade timeout, 10m by
default, the release first reports a failed upgrade and the move
finishes on the retry.

running pods keep their addresses. every agent restarts twice on
upgrade, once when the config change rolls the daemonset and once from
the hook, and while it restarts host to pod traffic on that node blips
for a few seconds because the router ip moves. a pod that sits on a
router ip today is reachable again after the move, the node no longer
holds that address. a pod on a node's current ingress ip keeps it and
that node keeps seeing it as `reserved:ingress`, so recreate it. the
hook logs those pods by name.

### Validation

- `helm unittest` passes cilium 23/23, platform 246/246 and kube-ovn
22/22. the new suites pin the rendered `cilium-config` keys per variant,
the hook shape, env and rbac, and the platform wiring for the talos and
generic branches, a custom pod cidr, host bits in the network address,
the refusals for a too small or non ipv4 cidr, and the slim variant.
- on a trial merge with main at cbc9d0c4e, where #3849 runs
`hack/*.bats` under real bats 1.14 with `set -u`, the 27 bats tests pass
after the second commit loads `test_helper`, the strict setup audit is
clean, and helm unittest passes cilium 23/23, platform 263/263 and
kube-ovn 22/22.
- `hack/cilium-ipam-migration.bats` passes 27/27 and runs the real
script against a fake kubectl that models the operator, the agent
daemonset and kube-ovn publishing the subnet status. thirteen mutations
turn it red each (exclusion after the move, no ingress exclusion, no
retry, no status check, an ingress ip inside the block listed on its
own, no `test` op, no wait for kube-ovn, no ip object check, no subnet
filter on the ip objects, no capacity check, a block outside the subnet
let through, overlap turned into containment, no log of ingress ip
holders). the containment one is pinned by a usage range that crosses
the start of the block, `10.244.247.250-10.244.248.3`.
- live on a 3 node talos test cluster with cozystack v1.6.4, cilium
1.19.5 and kube-ovn v1.15.26
- moved from `kubernetes` to the block, exclusion first and then the
node moves this script does. 3 nodes in about 40 seconds, every agent
took its router and ingress ip from its /29, no pod changed its address.
- gateway api through a nodeport 9/9 answered 200 and through the
loadbalancer ip 6/6, against 3/9 with `100.65.0.0/16`.
- the final hook job on the moved cluster is a no-op in 2 seconds,
including the ip object read with the job's own rbac. the same ip object
pipeline over that cluster's real objects finds 77 in a populated /24
and none in the block.
- the exact `test` plus `replace` patch on `ovn-default` goes through,
kube-ovn re-sorts the list right after, and the same patch against the
old list is rejected. that's the conflict path the retry covers.
- shellcheck is clean and `hack/check-commit-trailers.sh` passes.
- not run here. the upgrade move from an older release in ci, e2e only
covers the fresh install.

### Screenshots

not a ui change.

### Downstream repositories

walked the trigger map against the diff.
`packages/core/platform/values.yaml` only gains a comment, but
`networking.podCIDR` gets a new constraint on the kube-ovn variants,
which the hand written key table on the website should say. the ansible
role sets `networking.podCIDR` to `10.42.0.0/16`, which yields
`10.42.248.0/21` and needs nothing there. the talm preset keeps
`podSubnets: 10.244.0.0/16`, which no longer matters on these variants
because cilium stops reading `Node.spec.podCIDR` there. the terraform
provider doesn't model platform values.

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

### Release note

```release-note
fix(cilium)!: on the kube-ovn variants cilium now takes its router and gateway api ingress ips from the last /21 of networking.podCIDR, which kube-ovn no longer hands out, so pods stop landing on addresses cilium holds. existing clusters move on upgrade through a post-upgrade hook, and each cilium agent restarts twice, once for the config change and once in the hook. networking.podCIDR needs /20 or wider on these variants, and a /21 of /29s caps them at 256 nodes.
```


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

* **New Features**
* Kube-OVN-enabled variants now reserve the final `/21` of the pod CIDR
for Cilium router and Gateway API Ingress addresses, excluding it from
Kube-OVN pod allocation. Pod CIDRs must be IPv4 with a prefix of `/20`
or shorter.
* Added a post-upgrade migration that moves existing Cilium IP
allocations into the cluster pool and updates affected node networking.
* **Documentation**
* Documented the pod CIDR requirements and node capacity limit for
Kube-OVN variants.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-08 21:41:23 +03:00
Aleksei Sviridkin 76c030d89f fix(dashboard): drop the resource map ConfigMaps once the map is unreadable (#4789)
## What this PR does

Follow-up to #4612.

The ConfigMaps tab takes its names from the application's resource map.
React Query keeps the last successful list when a refetch fails, so when
the resource map became forbidden or went away after a successful read,
`useApplicationConfigMaps` went on parsing the old copy. The tab stayed
with the old names, every listed ConfigMap showed an access error, and a
malformed map kept its old error. A map that is unreadable from the
start already hid the tab.

Now a 403 or 404 on the resource map drops the names and the error
whether or not an earlier read succeeded, so the tab goes away in both
cases. Any other error is still shown on the tab, as before.

The new tests are a page test with stale resource map data next to a
403, and hook tests that read the map and then refetch it with 403 and
with 404, plus a malformed map followed by a 403. All four fail on main
and pass here. `pnpm typecheck`, `pnpm test` (451 tests in 62 files) and
`pnpm lint` pass.

### Screenshots

No visual change. Once the resource map is unreadable, the page looks
the same as for an application whose map was never readable, with no
ConfigMaps tab. The tab itself is shown in #4612.

### Downstream repositories

Walked the trigger map against the diff. It touches only the console
hook that reads the resource map and its tests. No chart, values,
schema, CRD or `hack/` file changes, and the console is vendored in this
repository.

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

### Release note

```release-note
fix(dashboard): the ConfigMaps tab now goes away when the application's resource map stops being readable, instead of keeping the names from the last successful read
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* ConfigMaps are no longer displayed when access to the resource map is
forbidden or the resource map is missing.
* The ConfigMaps tab is excluded when the resource map cannot be
accessed.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-08 16:49:58 +03:00
Aleksei Sviridkin f203a47afe fix(backups): hold a Postgres backup until the plugin rollout is over (#4674)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

The first BackupJob on a Postgres app whose `cnpg.io/Cluster` has never
carried the barman-cloud plugin — a fresh platform-flow app, or one an
in-place restore re-rendered — attaches the plugin and creates the
`cnpg.io/Backup` in the same reconcile. Attaching the plugin changes the
instance pod spec, so CNPG rolls every instance (`Pod rollout required:
volumes: element plugins has been added`). The Backup then lands on an
instance without the plugin sidecar (`requested plugin is not available:
barman-cloud.cloudnative-pg.io`) or on one CNPG restarts under it
(`instance manager was restarted during backup`). CNPG does not retry a
failed Backup, so the BackupJob stays Running in
`CNPGBackupTransientFailure` until `cnpgDefaultBackupDeadline` and
fails. The first backup of each such Postgres is always lost; this was
observed on a v1.6.3 cluster and reproduced against `main`.

The driver now holds the Backup back until that rollout is over. Before
attaching the plugin to a Cluster that has none, it records
`PluginRolledOut=False` on the BackupJob, with `Ready=False` reason
`WaitingForPluginRollout` for the user, and requeues; the next pass
attaches the plugin. The Backup is created once CNPG reports the Cluster
in its healthy phase and every instance pod (`cnpg.io/cluster`,
`cnpg.io/podRole=instance`) was created after the condition and is
Ready; the condition then turns True and `Ready` is cleared, as before.
Recording the condition first means a lost status write leaves the
plugin unattached for the next pass instead of a Cluster that looks
ready while it rolls. A Cluster that already runs the plugin is not
rolled by the driver and its Backup is created on the first pass, as
today. The wait is bounded by the existing `cnpgDefaultBackupDeadline`,
after which the BackupJob fails with the phase or pod CNPG is stuck on.

Why this signal, checked against the CNPG 1.29 source and the 1.30
Cluster CRD vendored here: the Cluster status has no
`observedGeneration` and its `Ready` condition is derived from
`status.phase`, so right after the attach the phase still reads healthy
from before until CNPG reconciles the new spec. `status.pluginStatus`
does not help either, since CNPG also lists plugins referenced only from
`externalClusters` there, which is exactly the state of a Cluster a
restore re-rendered. A pod spec change is never applied in place (only a
pending Postgres restart is), so the instances are recreated and pods
newer than the attach are the ones running the sidecar. This reads pod
metadata and CNPG's documented labels only, not the plugin's container
or volume names, and needs no RBAC change: the controller already lists
and watches pods.

Tests: `TestReconcileCNPG_WaitsForPluginRolloutBeforeBackup` drives a
Cluster without the plugin through the attach, a stale healthy phase
over the old pod, a rolling phase over the recreated pod, and the
healthy phase, and asserts no Backup exists until the last step.
`TestReconcileCNPG_AttachedPluginDoesNotDelayBackup` covers a Cluster
that already runs the plugin, and
`TestReconcileCNPG_PluginRolloutWaitIsBounded` the deadline. With the
gate disabled, the first and third fail; with only the phase checked and
not the pods, the first fails at the attach pass.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

Not applicable: no UI change.

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

The diff touches only `internal/backupcontroller` Go code: no package,
values, chart, CRD, `hack/` tooling, telemetry metric or contributor
convention. Walked against the trigger map, it reaches none of the
repositories below, and the website documents no workaround for the lost
first backup that this would make obsolete.

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

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(backups): the CNPG backup driver no longer loses the first backup of a Postgres app whose Cluster had no barman-cloud plugin yet (a new platform-flow app, or one restored in place). It now waits for CNPG to finish rolling the instances onto the plugin before creating the cnpg.io Backup, and reports the wait on the BackupJob as Ready=False with reason WaitingForPluginRollout.
```


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

* **Reliability**
* Backups wait for the backup plugin rollout to complete and cluster
instances to become healthy before starting.
* If rollout does not finish before the backup deadline, the backup job
fails instead of waiting indefinitely.
* Backups can start without waiting for a new rollout when the plugin is
already attached.
* **Backup Configuration**
* When no server name is configured, backups use the cluster name and
application UID, giving recreated applications distinct archive
prefixes.
* Existing server names are preserved when the plugin is already
attached.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-08 13:11:46 +03:00
Aleksei Sviridkin 9be997b6e2 fix(cilium): size the agent memory limit for its BPF maps
cilium sizes its CT, NAT, neighbor and socket rev-NAT maps as a share
of node RAM. They are preallocated LRU maps charged to the agent's
memcg, so on nodes with around a terabyte of RAM or more the maps alone
exceed the 4Gi limit and the agent is OOM-killed on every start after
a reboot, leaving the node without pod networking.

Raise the limit to the map ceiling (every map at LimitTableMax, both
address families) plus the agent's own 4Gi budget. A per-node limit
cannot be rendered into one DaemonSet, and pinning the map sizes would
cut connection capacity on large nodes and reset conntrack and NAT
state on upgrade, so the map sizes stay dynamic and a test keeps them
unpinned.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-07 19:10:40 +03:00
Aleksei Sviridkin 0a2bb5ef98 fix(cert-manager): enable the Gateway API solver in the host cert-manager (#4039)
## What this PR does

With the platform Gateway enabled, the ClusterIssuers in
`cert-manager-issuers` and the per-tenant Issuers rendered for
`TenantGateway` carry an `http01.gatewayHTTPRoute` solver, but the
controller refuses it: the vendored chart never turns Gateway API
support on, so every HTTP-01 challenge behind the Gateway fails with
`gateway api is not enabled` and no certificate is issued.

This passes a `ControllerConfiguration` with `enableGatewayAPI: true` to
the host cert-manager through the component values of its platform
Package. The host ships the Gateway API CRDs on every variant and orders
cert-manager after them, so the flag is safe there.

It is deliberately not a default of the `system/cert-manager` chart. The
same chart is installed into tenant Kubernetes clusters, where the CRDs
are an opt-in addon, and cert-manager v1.20 exits at startup when
Gateway API support is on and the CRDs are missing. The tenant
HelmRelease in `packages/apps/kubernetes` already passes the same
`ControllerConfiguration` when its `gatewayAPI` addon is enabled, so
both clusters now opt in from the layer that knows whether the CRDs are
there, and the chart itself stays neutral.

The new tests pin both halves: the host Package carries the flag and the
preconditions that make it safe, and the chart wires `config` through to
the controller while shipping none by default.

Closes #3725

### Screenshots

Not applicable, no UI changes.

### Downstream repositories

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

### Release note

```release-note
fix(cert-manager): enable the Gateway API solver in the host cert-manager, so ACME HTTP-01 challenges succeed when the platform Gateway is enabled
```


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

- **New Features**
- Supported system bundles can now use cert-manager’s Gateway API
HTTPRoute solver to issue certificates.
- Hosted deployments include the required Gateway API custom resource
definitions, installed before cert-manager.
- Gateway API solver support remains opt-in in environments that do not
provide the required definitions, avoiding unintended changes to
certificate management there.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-07 17:47:41 +03:00
Timofei Larkin 9b0e25b1a0 fix(etcd-operator): gate VMServiceScrape on VictoriaMetrics operator CRD (#3960)
## What this PR does

`packages/system/etcd-operator/templates/metrics.yaml` rendered a
`VMServiceScrape` (`operator.victoriametrics.com/v1beta1`)
unconditionally, so the `cozy-etcd-operator/etcd-operator` HelmRelease
fails to install on any cluster that does not have the VictoriaMetrics
operator CRDs, with `no matches for kind "VMServiceScrape" in version
"operator.victoriametrics.com/v1beta1"`. That made the VictoriaMetrics
operator a hard dependency of etcd-operator, which it should not be.

This wraps only the `VMServiceScrape` document in a
`.Capabilities.APIVersions.Has "operator.victoriametrics.com/v1beta1"`
gate, following the precedent already established in
`packages/system/linstor/templates/podscrape.yaml`. The
`etcd-operator-metrics` Service keeps rendering unconditionally, so any
scraper — a `VMServiceScrape` when the CRD is present, or something else
entirely — can still target the `metrics` port.

Note: this is a Cozystack-packaging-only issue. The upstream
etcd-operator repo's own chart is unaffected — it already gates its
scrape object.

Added `packages/system/etcd-operator/tests/metrics_test.yaml` (`helm
unittest`) pinning the new behavior: the Service always renders, and the
`VMServiceScrape` renders only when the capability is injected.

Kept this minimal — a pure capabilities gate with no new values key,
matching linstor's style, rather than adding a values toggle.

Follow-up (out of scope for this PR):
`packages/extra/etcd/templates/podscrape.yaml` (a `VMPodScrape`) is
unconditional the same way and would hit the same install failure on a
cluster without the VictoriaMetrics operator.
`packages/system/monitoring-agents` also ships several unconditional
`VMServiceScrape`/`VMPodScrape` objects, though that package may already
assume the VictoriaMetrics operator is present as part of the monitoring
stack it belongs to.

### Screenshots

Not applicable — no UI change.

### Downstream repositories

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

### Release note

```release-note
fix(etcd-operator): stop requiring the VictoriaMetrics operator to install etcd-operator; the VMServiceScrape now only renders when the operator's CRD is present
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* Metrics collection now works without the VictoriaMetrics operator API,
while scrape configuration is rendered when that API is available.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-07 16:54:10 +04:00
Aleksei Sviridkin 6192d5814b feat(local-storage): add opt-in OpenEBS LocalPV hostpath package (#3266)
## What this PR does

Adds `packages/system/local-storage`, an optional package that vendors
OpenEBS Dynamic LocalPV (chart 4.5.1) and registers it as the
`cozystack.local-storage` PackageSource. A cluster without the DRBD and
ZFS kernel modules can't run LINSTOR, so it has no dynamic PV
provisioning, hostpath volumes cover that. They also fit deliberately
node-local workloads.

- StorageClass is `cozystack-local-hostpath`, `WaitForFirstConsumer`,
`Delete` reclaim. It is not the default and doesn't take any existing
class name.
- Opt-in: a system bundle can list the package in
`bundles.enabledPackages`, no built-in variant enables it. Alone it
installs with `cozypkg add cozystack.local-storage`.
- Vendored RBAC is cut down by `patches/rbac.diff` (reapplied by `make
update`) to what volume reconciliation calls. Helper Pods, leader
election Leases and analytics state stay in the install namespace,
helpers are still privileged because they work on host directories.
- Images stay on docker.io, pinned by index digest with `make
update-image-pins`, nothing is rebuilt or mirrored.
- `local-storage` scope is mapped to `area/storage` in
`pr-labeler.yaml`, without it the title lands in `area/uncategorized` on
every run.

Tested with helm-unittest (StorageClass, PackageSource registration) and
`tests/rbac_test.py` (effective permissions across the bindings). Not
tried on a live cluster yet. Nothing installs the package by default,
the container cluster e2e lane that will use it is a follow-up
([#3238](https://github.com/cozystack/cozystack/issues/3238)).

### Screenshots

Not a UI change.

### Downstream repositories

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

Website follow-up is an issue, the docs there describe a package that
isn't merged yet. The diff adds a platform component and touches no
platform values, ApplicationDefinition, tooling or `hack/` path, so
nothing else in the trigger map matches.

### Release note

```release-note
feat(local-storage): add opt-in cozystack.local-storage package that installs OpenEBS LocalPV hostpath and provides the cozystack-local-hostpath StorageClass, it is not enabled by any built-in variant and is not the default StorageClass.
```


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

* **New Features**
* Added optional OpenEBS LocalPV Hostpath storage for provisioning
persistent volumes on node-local storage.
* Added a configurable `cozy-local-hostpath` StorageClass, disabled by
default as the cluster’s default class.
* Added controls for storage class creation, reclaim policy, hostpath
quotas, and node deployment.

* **Documentation**
* Added guidance on installation, configuration, storage behavior, and
uninstall considerations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-07 14:59:03 +03:00
Aleksei Sviridkin 76b9e55102 fix(cert-manager): enable the Gateway API solver in the controller
With the platform Gateway enabled, the ClusterIssuers in
cert-manager-issuers and the per-tenant Issuers rendered for
TenantGateway carry an http01.gatewayHTTPRoute solver, but the
controller only honours that solver when Gateway API support is
switched on, and the vendored chart leaves it off. Every HTTP-01
challenge behind the Gateway then fails with "gateway api is not
enabled" and no ACME certificate is issued.

Pass a ControllerConfiguration with enableGatewayAPI to the host
cert-manager through the component values of its platform Package.
The host always ships the Gateway API CRDs and orders cert-manager
after them, so the flag is safe on every variant.

It is not a default of the system/cert-manager chart: the same chart
is installed into tenant clusters, where the CRDs are an opt-in addon
and a controller started with Gateway API support and no CRDs exits
at startup. The tenant side keeps opting in on its own.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-by: LLM
2026-10-07 14:14:35 +03:00
Myasnikov Daniil d61fed46cc feat(local-storage): add opt-in OpenEBS LocalPV hostpath package
LINSTOR needs the DRBD and ZFS kernel modules, so a cluster without
them has no dynamic PV provisioning. OpenEBS LocalPV hostpath needs no
kernel modules and also serves deliberately node-local workloads.

The package is opt-in so it cannot change an existing cluster. Its
class is named cozystack-local-hostpath so it cannot collide with an
existing class name, and it is not the default so PVCs that omit
storageClassName keep their current binding. The vendored RBAC is
patched down to what volume reconciliation calls; helper Pods, Leases
and analytics state stay in the install namespace.

Images stay on docker.io, pinned by index digest.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 11:30:45 +03:00
Aleksei Sviridkin 6addef4d3c fix(kubevirt): route the export proxy to the Service KubeVirt creates (#4637)
## What this PR does

The VM export routes point at a Service that does not exist, and the
tenant network policy would block the Ingress even if it did.

KubeVirt creates the export proxy Service under the name
`virt-exportproxy` (`VirtExportProxyName` in
`pkg/virt-operator/resource/generate/components`, v1.9.0). The Ingress
and the TLSRoute that publish `vm-exportproxy.<host>` name a
`vm-exportproxy` Service, and nothing creates one. The backend was
`virt-exportproxy` when the Ingress was added in 0ddaff938. Then
3780925a6 renamed the hostname to `vm-exportproxy` and the backend along
with it. The hostname stays, only the backend goes back.

Renaming the backend is not enough for the Ingress, because it is served
by the ingress controller in tenant-root and tenant egress is
default-deny. Each allowed destination has its own `allow-to-*` policy.
The CDI upload proxy has `allow-to-cdi-upload-proxy`, the export proxy
had nothing. The new `allow-to-vm-exportproxy` policy renders for the
same tenants and the same source pods as the CDI one. Its destination is
narrower: only the `kubevirt.io=virt-exportproxy` pods in
`cozy-kubevirt`, on TCP 8443, the container port virt-operator gives
them.

The unit tests pin the backend name for the Ingress and the TLSRoute,
and the placement, selector and port of the new policy.

I checked this on a test cluster with KubeVirt v1.9.0 and ingress-nginx.
Without the fix the controller logs `Error obtaining Endpoints for
Service "cozy-kubevirt/vm-exportproxy"`. With the backend renamed and
the tenant allowed egress to virt-exportproxy, a passthrough request to
`vm-exportproxy.<host>` reaches the export proxy, and it answers with
its own serving certificate.

### Downstream repositories

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

### Release note

```release-note
fix(kubevirt): requests to vm-exportproxy.<host> now reach the KubeVirt export proxy: the Ingress and TLSRoute route to the virt-exportproxy Service KubeVirt creates, and tenants are allowed egress to the export proxy.
```


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

## Summary by CodeRabbit

* **New Features**
* Tenant workloads can now connect to the KubeVirt export proxy over TCP
port 8443.
* **Bug Fixes**
* Updated export-proxy Ingress and TLSRoute routing to use the
`virt-exportproxy` backend, keeping export-proxy traffic directed to the
correct service.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-07 11:19:32 +03:00
Aleksei Sviridkin 8c1a78dc9b feat(operator): default system namespace memory via LimitRange (#3494)
## What this PR does

System components now get a default container memory limit, so their pod
cgroups carry a `memory.max` and drop out of the Talos userspace OOM
handler's victim set. `cozystack-operator` maintains a LimitRange named
`cozystack-system-defaults` in every system namespace it reconciles.

The Talos handler (v1.12+) scores cgroups with `memory_max.hasValue() ?
0.0 : {Besteffort: 1.0, Burstable: 0.5, ...}[class] * memory_current`
and drops everything that scores zero. Only a memory limit sets
`memory.max`, so only a limit takes a pod out of the candidate set. A
request just moves a pod from BestEffort to Burstable, which under the
default `strictCgroupClassOrdering: true` still means killed second.
`PriorityClass` does nothing here, because the handler reads cgroupfs
and never talks to the apiserver. Victim selection is also unrelated to
the trigger, so a tenant workload thrashing against its own limit can
drive node memory pressure while a system component with no limit is
what gets killed.

An audit of a running cluster found around 70 workloads across ~30
packages with at least one container missing a memory limit. Per-chart
resources would not cover that set, and would not keep covering it as
packages are added. So the LimitRange is written by
`reconcileNamespaces`, which already creates these namespaces and
already tells system from tenant. It also covers init containers, which
#3495 can't reach: one of them without a limit leaves the pod cgroup
without `memory.max`, whatever the main containers declare.

Tenant namespaces are skipped. Where the tenant chart's own
`tenant-range-limits` exists (only when the operator configured
`resourceQuotas`), a second LimitRange next to it would leave the result
to the order LimitRanger iterates them. Where it doesn't exist, a tenant
namespace is not the place for the platform to impose a default.

### Why the default is 32Gi

The handler grants immunity for `memory.max` existing at all, whatever
its value. So the ceiling doesn't need to be fitted, and 32Gi is high
enough that nothing plausibly reaches it. A ceiling low enough to bind
would turn a rare pressure-driven kill into a deterministic one for any
component whose real usage nobody measured. This repo already has one
such case, recorded in the `vertical-pod-autoscaler` values, where a
110Mi limit OOM-killed the updater and failed HelmReleases with it.
Scheduling is unaffected, since pods are scheduled against requests and
the paired default request is 32Mi. An operator who wants the ceiling to
bind can lower it, per component in that component's chart or fleet-wide
with the knob, once real usage is measured.

### When the default is withheld

A default below a container's own request makes that pod impossible to
admit, so the operator checks before it applies. It scans Deployments,
StatefulSets, DaemonSets, CronJobs, Jobs and Pods in the namespace, init
containers included, for a container that requests more than the
configured limit and declares no limit of its own. If it finds one, it
withdraws the managed LimitRange and records a hold in the
`operator.cozystack.io/system-memory-state` annotation on the Namespace.

The hold does not clear by itself when the blocker disappears, because
an empty scan looks the same as a pod between deletion and recreation.
Lowering a previously applied limit also creates a hold, even with no
visible blocker, because LimitRanger writes the defaulted limit onto
admitted pods and the scan can no longer tell which containers declare
one. A cluster administrator releases the hold by copying the
`<hold>:<target>` token from the state into the
`operator.cozystack.io/system-memory-ack` annotation. The token is used
once and only for that hold and target. If a blocker is still visible,
the token is rejected and the hold stays. Package reconciles are
scheduled at least every five minutes, so an acknowledgement takes
effect on a quiet cluster too. The reason and the token also appear in
the operator log under `withholding the default container memory limit`.

### Namespace another LimitRange already governs

If a system namespace already has a LimitRange that says anything about
memory, at the Container or the Pod scope, the operator writes nothing
there and withdraws what it wrote before. A second default leaves the
effective value to iteration order. A foreign `max` below this default,
a `min` above the default request or a `maxLimitRequestRatio` rejects
pods outright, which takes out a whole namespace. At the Pod scope the
collision can't be decided in advance, because it depends on how many
containers a future pod has. A LimitRange that bounds only cpu or
storage coexists normally. A same-named LimitRange without the
operator's managed-by label is treated as foreign and left alone.

This matters most in `kube-system`, which is in scope because
`cozystack-scheduler` installs there, and where a distribution's or an
administrator's own LimitRange is likeliest to sit.

### Disabling

Setting `cozystackOperator.systemNamespaceMemoryLimit` to `0` disables
the feature. On the next reconcile the operator removes every LimitRange
it owns cluster-wide, found by the managed-by label. While the feature
is on, it also removes its LimitRange from namespaces no active Package
targets any more. The safety state on the Namespace stays in both cases.

### Docs

`docs/operations/system-memory-limits.md` landed in #3496 ahead of this
code, so main describes a `4Gi` default and installer values that do
nothing yet. The page is corrected to the 32Gi default and now describes
withholding, the acknowledgement, and the foreign LimitRange rule.

### Not in this PR

linstor policy. #3824 extends the controller's existing exemption from
limits to the satellite, and a linstor limit here would contradict it,
so linstor stays with that PR.

The scan reads six workload kinds through `APIReader`, uncached, on
every reconcile of every system namespace. That cost is tracked in
#3977, since the system namespace set isn't known when the cache is
built and the right fix depends on a reconcile-rate measurement nobody
has taken.

### Verification

`go vet` and `go test` pass for `cmd/cozystack-operator` and
`internal/operator`, and `helm unittest` passes on the installer chart
for the knobs unset, set, and the limit at `0`. The reconciler tests
cover withholding on an oversized request, the hold surviving the
blocker's removal, the hold on a lowered limit, acknowledgement and
token rejection, every memory field of a foreign LimitRange at both
scopes, ownership by label, the disabled sweep, and a deleting Package
not creating policy.

The withhold design has not been run on a live cluster yet. An earlier
design of this PR was, and that run does not carry over.

### Screenshots

Not a UI change.

### Downstream repositories

I walked the trigger map against the diff.
`packages/core/installer/values.yaml` gains two keys, but the
`ansible-cozystack` trigger is for a required key or a rename of one the
role passes, and these are new, optional and default to `""`, so the
role is unaffected. Nothing else in the map is reached by the Go
changes, the operator template or the page.

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

### Release note

```release-note
feat(operator): system namespaces now get a default container memory limit through a LimitRange maintained by cozystack-operator, which keeps system components out of the Talos userspace OOM handler's victim set. Tune it with cozystackOperator.systemNamespaceMemoryLimit (default 32Gi) and cozystackOperator.systemNamespaceMemoryRequest (default 32Mi); set the limit to 0 to disable it. The default is far above any real working set on purpose, because the handler grants immunity for a memory.max existing at all, not for its value. A namespace with a container that requests more than the limit and declares no limit of its own gets no default, and stays without one until a cluster administrator acknowledges the hold recorded on the Namespace (see docs/operations/system-memory-limits.md); lowering a previously applied limit needs the same acknowledgement. A namespace where another LimitRange already sets anything about memory is left alone. A container that declares its own memory limit is never changed.
```
2026-10-07 11:19:26 +03:00
Aleksei Sviridkin 84e8fcd477 fix(dashboard): re-authenticate on 401 instead of failing silently (#3115)
## What this PR does

When the dashboard session expires, the console now sends the user back
to sign-in instead of sitting on a stale page. This re-applies
cozystack/cozystack-ui#34 by @IvanHunters onto the in-tree console,
since the UI was vendored into the monorepo (#2963) and the standalone
repo is being archived. It is part of the cozystack-ui retirement
tracked in #3097.

When the `kc-access` cookie expires, k8s API calls return 401.
`K8sClient` now treats a 401 as an auth boundary and makes one top-level
navigation to `/oauth2/sign_in?rd=<current-url>`. That path is the
sign-in entry both gatekeeper modes serve. With OIDC enabled,
oauth2-proxy goes straight to the provider (`--skip-provider-button`)
and returns the user to `rd`. Without OIDC, token-proxy renders its
token form there, but it does not honour `rd` yet, so the user lands on
`/` after sign-in.

The handler can be replaced through `K8sClientConfig.onUnauthorized` and
does nothing outside the browser. A latch keeps concurrent 401s from
redirecting more than once, and any 2xx from either fetch site re-arms
it, so a handler that re-authenticates in place still sees the next
expiry.

Related: #2788.

### Verification

`client.unauthorized.test.ts` covers the hook on the request and watch
paths, the latch across concurrent 401s, the re-arm after a success, and
the default handler's target and relative `rd`. `pnpm typecheck` and the
console test suite pass.

Original PR: cozystack/cozystack-ui#34, credited via `Co-authored-by`.

### Screenshots

Not a UI change.

### Downstream repositories

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

### Release note

```release-note
fix(dashboard): on a 401 (expired session) the console now sends the user to sign in again instead of silently showing a stale page. With OIDC enabled the user returns to the page they were on; without OIDC they land on the start page after entering a token.
```
2026-10-07 11:14:37 +03:00
Yan Bondarenko 8ce3f853af fix(dashboard): drop the resource map ConfigMaps once the map is unreadable
React Query keeps the last successful list when a refetch fails. After the
resource map became forbidden or went away, the hook went on parsing that
old copy, so the ConfigMaps tab stayed with stale names, and a malformed
map kept its old error, instead of the tab going away as it does when the
map is unreadable from the start.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
2026-10-07 11:00:18 +03:00
IvanHunters 3cce629535 feat(redis,postgres): certify the per-engine external hostname (#4627)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

Redis and postgres server certificates now carry the per-engine external
name `<release>.<engine>.<apex>` (for example
`redis-foo.redis.example.org`) next to the flat `<release>.<apex>` they
already had. Both names appear under the same condition as before: TLS
on and `external: true`, with a tenant apex present.

A TLS-passthrough listener on the tenant Gateway forwards the handshake
untouched, so the certificate the client checks is the engine's own. The
design in #2815 routes databases by SNI under `*.redis.<apex>` and
`*.postgres.<apex>`, and without the per-engine name in the SAN list a
verifying client would fail the hostname check. Issuing both names now
means a later move between the two hostname schemes reissues no
certificate and keeps clients on the same trust anchor. This is the
certificate step of #2815. Nothing renders a route yet.

The SAN list changes, so an existing external TLS release gets its
server certificate reissued once on upgrade, from the same CA. For redis
the operator then rolls the Redis and Sentinel pods one at a time, the
same way it handles a renewal.

The READMEs say who can reach the new name through a passthrough
listener. For postgres that is only clients using direct TLS negotiation
(server 17+, `sslnegotiation=direct`), because by default a plaintext
SSLRequest comes before the handshake. `redis-cli` needs `--sni`.

MongoDB is not in this PR. PSMDB issues the certificate itself and its
CRD has no field for extra SANs, so adding the mongos name means the
chart taking over the certificate. That needs its own change.

Covered by helm-unittest: the name is present with TLS and external on,
and absent with external off, with TLS off, and in a tenant with no
apex.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

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

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
feat(redis,postgres): server certificates of external redis and postgres releases also carry the `<release>.<engine>.<apex>` name that the tenant Gateway's TLS-passthrough listeners are designed to route by. Existing external TLS releases get their server certificate reissued once on upgrade, from the same CA.
```


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

* **New Features**
* PostgreSQL TLS certificates now cover both the tenant hostname and the
per-engine `postgres` hostname for external connections.
* Redis TLS certificates now also cover the per-engine `redis` hostname
when external services are enabled.

* **Documentation**
* Clarified connection requirements for per-engine hostnames: PostgreSQL
routing requires direct TLS negotiation, PostgreSQL 17 or later, and a
compatible client; Redis clients must specify SNI.
* Clarified that the hostnames may need to be mapped manually to the
LoadBalancer address.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-07 10:56:34 +03:00
Aleksei Sviridkin b1c84e9c06 feat(platform, tests): select E2E packages and declare controller dependencies (#4022)
## What this PR does

E2E installs the whole platform on a fresh cluster for every PR,
whatever the PR touches, so install time and flake surface go to
packages the selected suites never use. This PR makes the E2E install
take only what the selected suites need.

`hack/select-install.sh --disabled <suites>` generates
`bundles.disabledPackages` for fresh E2E installs using an ordinary
platform variant. It keeps suite owners, runtime requirements and the
install baseline, then follows their forward `PackageSource`
dependencies.

The baseline follows the unconditional waits in
`hack/e2e-install-cozystack.bats` and stays seeded directly even where
the graph reaches the same packages. It also keeps platform services
that no suite owns but the rest of the platform uses: `multus`,
`cozystack-scheduler`, `linstor-scheduler`, `flux-plunger`, `cozy-proxy`
and `ouroboros` (the Kubernetes suites assert its hairpin NAT). Backup
suites also keep `backupstrategy-controller` and its dependencies, and
the gateway suite keeps `kubeovn-webhook`.

The E2E job now selects the suites before it installs, and passes the
complement to the platform Package as `disabledPackages`. The `full-e2e`
label and an empty selection keep the full install, and the nightly and
tag lanes pass no list.

The platform sources also change: `mongodb-application` now depends on
`mongodb-operator`, and `seaweedfs-application` on
`objectstorage-controller`. These dependencies apply to regular platform
installs and upgrades too.

The new `mongodb-operator` edge also reaches reverse test selection: a
change under `packages/system/mongodb-operator` now selects the
`mongodb` suite instead of falling back to the full suite. Backup APIs
and the gateway webhook remain suite-specific install requirements, so
they add no edges.

The disable list governs fresh installs because existing Packages carry
`helm.sh/resource-policy: keep`. Optional packages still need
`bundles.enabledPackages`. Packages that no suite owns and the baseline
does not keep stay uninstalled on purpose, also when test selection
answers with every suite (monitoring exporters like `goldpinger` and
`kubeovn-plunger`, apps without a suite, provider-specific operators).
Nothing in the run looks at them, so installing them only adds a
readiness wait, and a change to such a package gets no install check
from this gate.

### Measured

One run per variant on a 32 vCPU / 128 GB VM, container lane, the CI
artifacts of this PR, `postgres` suite selected, 55 packages disabled:

- HelmReleases 111 to 77, pods 219 to 197
- pod requests 14.8 CPU / 37.7 GiB to 13.5 CPU / 35.7 GiB
- host memory peak during install 33.4 GB to 30.2 GB
- `install-cozystack` 26m33s in both runs
- `postgres` suite 25m50s to 25m17s, passes on the reduced install

Install time did not change. The removed packages are mostly operators
and apps without instances, and what the install waits on (SeaweedFS,
LINSTOR, CDI, the Cluster API providers, Keycloak) stays in the
baseline. With one run each, differences of a minute or two are noise.

The selection, the install closure and the workflow wiring are covered
by bats tests (`hack/select-install_test.bats`,
`hack/select-e2e_test.bats`, `hack/platform-packages_test.bats`) and by
a helm-unittest suite in the platform chart.

### Screenshots

Not a UI change.

### Downstream repositories

The new dependencies ship in the platform chart. No coordinated changes
are needed in downstream repositories.

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

### Release note

```release-note
feat(platform, tests): E2E installs only the packages the selected suites need. MongoDB now depends on its operator, and SeaweedFS on the object storage controller.
```
2026-10-07 09:36:24 +03:00
Aleksei Sviridkin 5486d4ffcf fix(networking): harden webhook policy assumptions (#4058)
## What this PR does

This is follow-up for
[#3997](https://github.com/cozystack/cozystack/pull/3997). It reads
ingress-nginx admission port from effective subchart values and refuses
empty value, and fixes `kubeProxyReplacement=false` status because
cozystack has no supported Service translator in that state. Gateway
test now requires one real ClusterIP and keeps same ready Pod UID, IP
and port for whole probe, so stale EndpointSlice, replaced Pod or wrong
Service can't report deny. Policy selectors and two-ended deny from
[#3997](https://github.com/cozystack/cozystack/pull/3997) stay
unchanged, and this does not claim root cause for
[#3983](https://github.com/cozystack/cozystack/issues/3983).

### Screenshots

Not applicable.

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

### Release note

```release-note
fix(networking): use effective ingress admission port, report unsupported Cilium service routing correctly, and reject stale backend evidence in gateway tests
```


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

- **Bug Fixes**
- Admission webhook connectivity checks now validate the configured
Service and its exact backend endpoint throughout probing, improving
failure detection.
- Admission webhook policies now fail closed when the webhook port is
not explicitly configured.

- **Documentation**
- Clarified that supported service routing requires
`kubeProxyReplacement: true`.
- Updated guidance to explain that disabling this setting or webhook
policies does not provide a supported fallback.

- **Tests**
- Added coverage for missing webhook port configuration and strengthened
networking stability checks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-07 08:37:53 +03:00
IvanHunters ac4ab1c4a6 chore: drop private cluster and host names from the tree (#4461)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `kind/backport` if it's a bugfix that needs to be
backported to a previous version.
-->

## What this PR does

I remove internal cluster names, a real hostname with its IP addresses,
internal mirror hostnames and an internal product name from comments, an
example file, test fixtures and a dead commented-out config block. Where
a stand-in is needed I use neutral test-cluster wording, RFC 2606
domains or RFC 5737 documentation addresses. I also replace two real
Keycloak usernames and a reviewer's name in code comments with
placeholders, or drop them. Rendered output does not change. The test
fixture hostnames change on the input and the expected side together,
and the unit tests of every touched package still pass.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

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

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
NONE
```


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

* **Documentation**
* Updated backup, OIDC, autoscaling, and platform references to describe
test environments without naming specific development clusters.
* Replaced development-specific endpoints in the platform example with
`example.org` and documentation-only IP addresses.
  * Updated OIDC username examples.
* **Tests**
  * Updated fixture hostnames and expected URLs to use `example.org`.
* **Chores**
* Removed commented-out registry mirror entries; the active mirror
configuration is unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-07 08:05:19 +03:00
Aleksei Sviridkin bc9a594e47 test(clickhouse): add backup API render assertions (#4011)
## What this PR does

This adds unit tests for how the ClickHouse chart wires up the backup
API that the default backup strategy calls. The Pod of the
`cozy-default-altinity` strategy reaches the clickhouse-backup sidecar
on the named port `ch-backup-api`. It authenticates with the credentials
from the `<release>-backup-api-auth` Secret.

The new suite is
`packages/apps/clickhouse/tests/backup_api_contract_test.yaml`. It
checks that:

- the sidecar exposes `ch-backup-api` on 7171/TCP;
- `API_USERNAME` and `API_PASSWORD` come from the `username` and
`password` keys of `<release>-backup-api-auth`;
- that Secret renders with `username: backup` and a non-empty password;
- no auth Secret renders when backups are disabled.

All of this is rendered by the chart, so the unit tests catch a
regression on every pull request. The e2e assertions on the same wiring
run only when test-impact analysis selects the ClickHouse suite. They
stay as they are in `hack/e2e-chainsaw/clickhouse`, because they check
the operator-created Pod and the admitted Secret, and rendering can't do
that.

### Screenshots

Not a UI change.

### Downstream repositories

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

### Release note

```release-note
NONE
```
2026-10-07 06:58:44 +03:00
Aleksei Sviridkin a1416655f4 fix(tenant): allow tenant egress to the KubeVirt export proxy
The vm-exportproxy Ingress is served by the ingress controller in
tenant-root, and tenant egress is default-deny. Nothing allowed traffic
to virt-exportproxy in cozy-kubevirt, so the Ingress stayed unreachable
even with the right backend Service. The new policy renders wherever
allow-to-cdi-upload-proxy does, but narrows the destination to the
export proxy pods and their container port.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-07 06:23:28 +03:00
Aleksei Sviridkin c069a191fc fix(monitoring-agents): give kube-state-metrics a sized memory limit (#3846)
`kube-state-metrics` ran BestEffort with no resources at all: the
subchart ships `resources: {}` and our wrapper never set any. So it put
no `memory.max` on its pod cgroup, and the Talos userspace OOM handler
only ever selects cgroups that lack one, which left it permanently in
the victim set. Any tenant workload driving node memory pressure could
evict the source of every kube-state metric we alert on. Background is
in `docs/operations/system-memory-limits.md`.

## The numbers are measured

On a three node cluster with 322 pods and roughly 25.6k series, which is
the smallest we run: 14 day peak working set 307Mi, peak RSS 280Mi, mean
176Mi.

So the request sits above the mean at 192Mi rather than at the peak, and
the limit carries wide headroom at 2Gi. The headroom is deliberate: this
component's memory scales with the number of objects in the cluster, and
the `--metric-labels-allowlist=pods=[*],deployments=[*]` already in this
file widens every pod and deployment series further, so a cluster larger
than the one measured lands well above what was seen here. `2Gi` is 6.7x
that peak, which read as linear in object count is roughly 2100 pods and
170k series, so the limit stays a ceiling rather than a bound well past
the largest cluster this sizing was drawn from.

No CPU limit, following the node DaemonSet sizing in `c9ba7a3cc`, which
set one zero times across nineteen files. CPU is irrelevant to the OOM
ranking, and a ceiling that binds on a scraper turns into scrape
timeouts rather than backpressure.

## This supersedes #2632

#2632 proposes 256Mi, which is below the 307Mi peak measured on the
smallest cluster we have, so it would convert an eviction risk into a
reliable OOM kill. It also adds a `cpu: 200m` limit, which is the half
that buys nothing. The diagnosis there was right and it sat since May;
the numbers are what needed replacing.

## Checks


`packages/system/monitoring-agents/tests/kube_state_metrics_resources_test.yaml`
pins the limit, both requests, and the absence of a CPU limit. Mutation
checked: dropping the limit to 256Mi turns it red. The chart's suites
are 3 files and 6 tests, all green.

### Release note

```release-note
fix(monitoring-agents): kube-state-metrics now carries a memory request and limit sized from a measured peak, so it is no longer a permanent candidate for the node OOM handler
```


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

* **New Features**
* Configured kube-state-metrics with requests of 50m CPU and 192Mi
memory, plus a 2Gi memory limit. No CPU limit is configured, allowing
CPU usage to scale as needed. These settings help ensure the service has
the memory and CPU resources it needs while keeping memory use bounded.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-07 06:19:17 +03:00
Myasnikov Daniil 5b585d6eda fix(operator): persist holds for unsafe memory defaults
An admitted Pod loses the distinction between explicit and defaulted
limits. Its disappearance cannot prove a lower default is safe for
its replacement. Keep that decision on the Namespace until an
administrator acknowledges the particular target, without waiving
current blockers or foreign policy ownership.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:06:51 +03:00
Myasnikov Daniil 30ecc41070 fix(operator): close memory policy safety gaps
Preserve foreign same-name objects and keep create-time SSA non-force.

Only ignore failed pods controlled by terminal Jobs.

Make the failure-containment test inject Apply errors for real.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:06:51 +03:00
Myasnikov Daniil 6a521f0c68 fix(operator): sweep memory defaults when disabled
List LimitRanges by the ownership label once per Package reconcile when
the feature is disabled, and remove managed objects even in namespaces
outside the current Package target set. Skip the per-namespace path while
disabled and keep cleanup failures non-fatal to namespace reconciliation.

Update the flag help, installer guidance, and operations documentation to
describe the global cleanup boundary.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:06:49 +03:00
Myasnikov Daniil f6dcb69b61 refactor(operator): withhold instead of raising, and drop the grace period
The raise-and-grace machinery cannot work: LimitRanger writes the
raised limit onto the very container that justified the raise, so at
that container's next admission it carries a limit, the scan skips
containers that have one, and the raise has erased its own evidence.
The ceiling then drops on schedule and the workload is rejected
permanently. Any such machinery approximates admission logic ahead of
admission, and that is incomplete by construction: a
LimitRange default is one number for a whole namespace and cannot mean
"the configured limit, or this container's request, whichever is
larger". Only per-pod conditional defaulting expresses that, which is a
mutating webhook and not this.

The justification for the machinery had also expired. It was designed
when the default was 4Gi, where withholding was self-perpetuating for
VPA-managed namespaces. At 32Gi nothing in the tree approaches the
ceiling -- the largest static memory request is 2Gi -- and VPA sets
requests and limits together, so those containers carry their own limit
and were never blockers.

So a namespace holding a container that requests more than the
configured limit with no limit of its own is now left with no default at
all, and anything written there earlier is withdrawn. It fails in the
safe direction, which is the whole argument: a namespace with no
memory.max is a namespace behaving as it did in every release before
this, while a default below a request is a namespace whose workload
cannot be admitted. It needs no stored state and no timer, because
withholding is not a decision that has to survive between reconciles --
once the oversized request is gone the default is applied again on the
next pass.

Gone with it: the grace period, the clear-since annotation and its
clamp, the injected clock, containerMemoryDefault, raisedCeiling, and
fourteen tests. The scan stays, because deciding whether to apply or
withhold is what it was always for. The foreign-LimitRange ownership
guard, the disable path and the 32Gi default are untouched.

The residual sharp edge is stated rather than machinery: a workload
whose only evidence is a live Pod, built from a custom resource with no
template this scan can read, can still be missed if the scan runs while
that Pod is absent, and applying the default then rejects its
recreation. At 32Gi that is unreachable in this tree. It becomes
reachable if an operator lowers the knob below such a workload's
request, which the flag help, values.yaml and the operations page now
say plainly.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:06:48 +03:00
Myasnikov Daniil 81c12bc953 fix(operator): withhold on any foreign memory policy, not two fields
The coexistence guard treated a namespace as already-governed only when
another LimitRange carried a container memory default or max, and said
so in its comment. That is not the set of fields that collide with what
this reconciler writes. A container min above the default request
rejects the pod, and so does a maxLimitRequestRatio, since a 32Gi
default against a 32Mi default request is a ratio of 1024:1 and no cap
allows it. So the namespace-wide admission failure the guard exists to
prevent stayed reachable through fields the guard did not read, and
kube-system is where it is likeliest, this feature having deliberately
brought it in scope.

Enumerating the colliding fields does not fix it, because at the Pod
scope the collision is not decidable here. Whether a per-container
default of 32Gi breaches a pod-wide max depends on how many containers
the pod has, and the pods in question do not exist yet: any arithmetic
about them is a guess, and a wrong guess costs a namespace its entire
pod admission. So the rule is ownership rather than arithmetic. If
anything else in the namespace has an opinion about memory, at either
scope and in any field, the namespace is not this reconciler's to
default. Narrow still on the resource, because nothing here writes cpu
or storage and a LimitRange bounding those must keep coexisting. This
is the rule the tenant-namespace exclusion already rests on.

The pod scan no longer skips Failed pods, only Succeeded. A pod that
ran to completion is never recreated from its spec, but an eviction or
a node crash leaves a Failed carcass holding the same oversized request
that its controller will recreate, and that carcass is frequently the
only surviving evidence the namespace needs a raised ceiling, since the
shape the scan looks for cannot be admitted once the namespace has
settled. Dropping it lowered the ceiling under a workload that was
coming back, which is what the grace period exists to prevent.

A grace stamp dated ahead of the clock is clamped to now. Unclamped it
held the ceiling for the skew plus the grace period, silently, so an
hour of skew bought ninety minutes instead of thirty. Clamping never
shortens the wait, it only stops it being extended.

The two flag defaults are named constants the flag registration and its
test both read. The test previously duplicated the literal and still
asserted 4Gi after the default moved to 32Gi, so it misinformed a
reader about the shipped value and pinned nothing.

The disable claim is narrowed to what the code guarantees. "Removes any
previously created one" was universal, but the reconciler only runs for
namespaces an active Package still targets, so a namespace whose
Package was removed keeps its LimitRange. The flag help, values.yaml
and the operations page now say that, and name the managed-by label for
finding leftovers by hand.

The test asserting a Pod-scoped memory max should be ignored is gone,
since under the ownership rule a Pod-scoped memory bound withholds.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:06:45 +03:00
Myasnikov Daniil bcfa50d7e9 test(e2e): install only the packages the selected suites need
The E2E job installs the whole platform on a fresh cluster before it
knows which suites will run, so every PR pays install time and flake
surface for packages its suites never use. Select the suites first and
pass the complement computed by select-install.sh to the platform
Package as disabledPackages.

The full-e2e label and an empty selection keep the full install, and
the nightly and tag lanes pass no list, so they are unchanged.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:01:53 +03:00
Myasnikov Daniil 974aaa0bac test(hack): regenerate the selective install fixture
Two optional Cluster API providers, Proxmox and in-cluster IPAM, were
added to the platform sources after the fixture was generated. The
drift test compares the fixture with the current disable list, so it
failed until they were added.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:01:53 +03:00
Myasnikov Daniil f46a9797b8 fix(hack): keep platform services in selective installs
Dropping every package that no suite owns also drops services the rest
of the platform relies on: multus for secondary networks, the cozystack
and LINSTOR schedulers, flux-plunger, cozy-proxy and ouroboros, whose
hairpin NAT the Kubernetes suites assert. Nothing waits for them, so no
PackageSource edge reaches them, and adding one would also change
reverse test selection.

The remaining packages stay uninstalled on purpose, also when test
selection answers with every suite: nothing in the run looks at them,
so installing them only adds a readiness wait.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:01:52 +03:00
Myasnikov Daniil 59555c895e fix(hack): retain controllers and APIs for selective installs
Tenant HelmReleases need a shard reconciler, and the root SeaweedFS
release creates COSI resources even when no backup suite is selected.
Without those controllers, a fresh reduced install cannot become ready.

The Kubernetes suites create worker pools through KubernetesNodes, and
MongoDB renders an operator-owned CR. Selecting only the cluster API or
application chart drops resources those suites require.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:01:52 +03:00
Myasnikov Daniil 361893e6f4 fix(hack): retain backup APIs in selective installs
Backup roundtrip suites create Bucket and backup resources that the
application graph does not require. A graph-only closure therefore
omits the APIs those suites need on a fresh install.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:01:51 +03:00
Myasnikov Daniil 610578be16 test(hack): verify selective install contract
A missing suite owner or a discarded discovery failure can leave a
reduced install without the APIs its tests exercise. Graph-only checks
cannot establish that the chart preserves the requested package set.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 06:01:50 +03:00
Myasnikov Daniil b6c33926dc fix(dashboard): re-arm the 401 latch once the session recovers
unauthorizedHandled was set once per client and never cleared, so it
collapsed every later 401 too, not just the concurrent ones it was meant
to. Harmless for the shipped default handler, which navigates away and
takes the client with it, but the onUnauthorized hook exists precisely so
a caller can re-auth in place, and such a handler would fire exactly once
for the client's lifetime and then go silent.

Clear the latch on any 2xx from either fetch site: a success means the
session is alive again, so the next 401 is a new episode. The storm guard
still holds, since concurrent 401s have no success between them.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 05:58:15 +03:00
Myasnikov Daniil 7e5f9ab152 fix(dashboard): re-auth via /oauth2/sign_in so token-proxy mode works
The dashboard gatekeeper runs one of two mutually exclusive proxies,
chosen by the cluster's oidc-enabled flag (templates/gatekeeper.yaml).
/oauth2/start exists only in oauth2-proxy. The token-proxy branch
registers only sign_in, sign_out and userinfo under the /oauth2
prefix, so there /oauth2/start falls through to the catch-all
reverse-proxy handler, which does not re-verify the token. With a
present but expired cookie it proxies the request to the console
upstream, the SPA reloads, gets a 401 again and loops.

/oauth2/sign_in is the one entry point both modes implement, so the
default 401 handler goes there. oauth2-proxy's sign_in hands straight
off to OAuthStart because the gatekeeper passes --skip-provider-button,
and it resolves rd through the same app-director, so the OIDC path
behaves as it does through /oauth2/start. token-proxy serves its token
form there. It does not honour rd yet, so after sign-in the user lands
on / instead of looping.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 05:58:14 +03:00
Myasnikov Daniil badbec63fe test(dashboard): cover the 401 -> oauth2-proxy redirect path
The 401 handling is what brings an expired session back to sign-in,
and a regression in it stays invisible until a cookie expires. The
test drives K8sClient through the vi.stubGlobal("fetch", ...) harness
the sibling k8s-client tests use. It checks that the hook fires once
per 401 from both the request and the watch fetch sites, once across
concurrent 401s, never on other responses, and that the default
handler passes the current location as a relative rd, which
oauth2-proxy's open-redirect check accepts.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 05:58:14 +03:00
Myasnikov DaniilandIvanHunters 8bf78d3bfe fix(dashboard): redirect to oauth2-proxy on 401 instead of failing silently
When the kc-access cookie expires, k8s API calls return 401 and the
SPA stays on a stale page until the user clears the cache by hand.
K8sClient now treats 401 as an auth boundary and makes one top-level
navigation into the oauth2-proxy sign-in flow, passing the current
location as rd so the user comes back to the same page. The handler
can be replaced through K8sClientConfig.onUnauthorized and does
nothing outside the browser. The unauthorizedHandled latch keeps
concurrent 401s from redirecting more than once.

Re-applied from cozystack/cozystack-ui#34 onto the in-tree console
after the UI was vendored (#2963), since the source repo is being
archived. Related: #2788.

Co-authored-by: IvanHunters <49371933+IvanHunters@users.noreply.github.com>
Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 05:58:13 +03:00
Myasnikov Daniil 3d901ca9b4 test(clickhouse): add backup API render assertions
The port the backup strategy dials, the two secretKeyRefs the
clickhouse-backup sidecar authenticates with, and the auth Secret that
backs them are all rendered by this chart. A helm-unittest suite
catches a regression in them on every pull request, while the e2e
assertions on the same wiring run only when test-impact analysis
selects the ClickHouse suite. The e2e assertions stay, since they
check the operator-created Pod and the admitted Secret.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 05:52:28 +03:00
Myasnikov Daniil 9fe93a3d7e docs(operator): fix three claims that overstated what the code does
The static-pod note said containers built from an on-disk manifest are
unaffected "in both directions". True of the container, which the
kubelet runs from disk whatever the API says, but the mirror pod is an
ordinary API object that passes admission like any other, so a static
pod declaring a memory request above the ceiling with no limit of its
own has its mirror pod rejected -- leaving the container running with no
representation in the API. Worth naming, since kube-system is in scope
by design. The scan does cover it: a mirror pod is a live Pod.

The ReplicaSet exclusion was justified on live ones being copies of
their Deployment's template. That does not reach an ownerless
ReplicaSet, which at replicas 0 has neither pods to be seen through nor
a Deployment template standing in for it. It stays out of scope --
nothing here ships a bare ReplicaSet -- but as a gap left open rather
than one the stated reason closes.

The delete guard's comment claimed the enabled apply always adopts and
stamps a same-named foreign object. It does not when that object's spec
already matches, because then the apply is skipped entirely. The
outcome is the same one the guard gives on purpose, but it is reached by
the apply not running.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 05:36:54 +03:00