Commit Graph
7827 Commits
Author SHA1 Message Date
Aleksei Sviridkin da7f2c2f50 fix(tests): keep two unit tests green after the release image restamp (#5180)
## What this PR does

Same change as #5179, on `main`. Both tests pass here today, because
`main` still carries the pre-release image pins; they fail on every
release line as soon as the first rc is cut, since the `Prepare release`
commit rewrites `packages/*/values.yaml` and two unit tests read exactly
the fields it rewrites. On `release-1.7` this has kept `Unit &
controller tests` red on every PR since v1.7.0-rc.3
([job](https://github.com/cozystack/cozystack/actions/runs/41162009892/job/124421449910)
on #5172), and `release-1.8` would repeat it.

| test | reads | before the release build | after (release-1.7 today) |
|---|---|---|---|
| `hack/cni-plugins-staging-contract.bats` "the kube-ovn release the CNI
plugin pin was checked against is the one vendored" |
`.global.images.kubeovn.tag` in `packages/system/kubeovn/values.yaml` |
`v1.15.26@sha256:…` | `v1.7.0@sha256:…` |
| `packages/system/monitoring/tests/vtraces_collector_test.yaml` "pins
the collector image through the cozy-lib.image helper" |
`tracingCollector.image` |
`docker.io/otel/opentelemetry-collector-contrib:0.136.0@sha256:…` |
`ghcr.io/cozystack/cozystack/otelcol-tracing:v0.136.0-cozystack.0@sha256:…`
|

Changes:

- The bats test takes the vendored kube-ovn release from `appVersion` in
`packages/system/kubeovn/charts/kube-ovn/Chart.yaml`. That is the value
`packages/system/kubeovn/Makefile` builds the image from, and a release
build leaves it alone. The hint printed on a mismatch names the new
file.
- The helm unittest case no longer names the image at all. It sets
`_cluster.images-registry` and asserts that the registry lands in front
of the rendered image and that the image stays digest-pinned (the
`grafana_upstream_image_plugins_test.yaml` pattern). Removing the
`cozy-lib.image` call from the template makes it fail; the old regex did
not.

Same single commit as #5179; the only difference is the unrelated `DOC=`
line next to the edit, kept as `main` has it.

Verified locally on this branch: `bats
hack/cni-plugins-staging-contract.bats` 10/10, `helm unittest
packages/system/monitoring -f tests/vtraces_collector_test.yaml` 28/28.

Refs #5178 (the release-1.7 report), #5179, #5172.

### Downstream repositories

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

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

* **Tests**
* Version validation now compares the checked version with the chart’s
application version and reports missing or mismatched versions with the
chart path.
* Monitoring image checks now verify use of the configured mirror
registry and require a 64-character lowercase SHA-256 digest, without
requiring a specific image name.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:19:21 +03:00
Aleksei Sviridkin 23461e065d fix(redis): run one Sentinel when Redis runs a single pod (#5151)
<!-- 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 chart pins `spec.sentinel.replicas: 3` whatever `replicas` says, so
an instance created with `replicas: 1` runs one Redis pod and three
Sentinel pods that have nothing to fail over to. This makes the Sentinel
count follow the data pods: one Sentinel when `replicas` is 1, three
otherwise.

The operator cannot run zero Sentinels (freshworks `Validate()` resets
`sentinel.replicas <= 0` to 3), and it needs at least one to locate the
master. With one Sentinel its quorum is `replicas/2+1` = 1, so the
single Sentinel works on its own.

The operator also gives the Sentinel Deployment a PodDisruptionBudget
with `minAvailable: 1` whenever Redis has two replicas or fewer
(`ensurePodDisruptionBudget` in
`operator/redisfailover/service/client.go`). On a single Sentinel that
budget refuses every eviction and would block node drains, so the chart
sets `sentinel.disablePodDisruptionBudget: true` in that case only. The
Sentinel `WorkloadMonitor` now expects the rendered count instead of a
fixed 3/2.

The operator only stops ensuring that PDB once the flag is set; it never
deletes the one it created while three Sentinels ran
(`EnsureSentinelDeployment` in freshworks v3.3.5). An existing instance
scaled to one replica would keep it, so
`templates/hooks/sentinel-pdb.yaml` adds a post-upgrade hook that
deletes `rfs-<release>`, with a Role scoped to that one name. It renders
on upgrade only, since a fresh one-replica install never gets that PDB.

From `replicas: 2` up nothing changes. Scaling an existing instance
between 1 and 2 replicas scales the Sentinel Deployment between 1 and 3
accordingly.

Not in scope: with `replicas: 1` the Redis data pod keeps its own
operator PDB (`minAvailable: 1`), which already blocks drains of its
node today; that is unchanged here.

Tests: `tests/sentinel_replicas_test.yaml` covers 1, 2, 3 and the
default, the PDB switch, and the `WorkloadMonitor`. Three of its cases
fail on `main` without the template change.
`tests/sentinel_pdb_hook_test.yaml` covers the hook. `helm unittest
packages/apps/redis`: 79 passed. Open PR #4990 also edits
`redisfailover.yaml` (Sentinel spreading); the two touch different keys.

Backport: `release-1.7` ships the same freshworks operator and CRD, so
`kind/backport` applies. `release-1.6` is not backportable as is: it
still runs spotahome `v1.3.0-rc1`, whose vendored CRD (chart 3.3.0) does
not declare `sentinel.disablePodDisruptionBudget`, and the operator's
CRDs are installed with Flux's default `crds: Create` policy, so they
are never updated on an existing cluster. The API server would prune the
field and the single Sentinel would keep a `minAvailable: 1` PDB that
blocks node drains. A 1.6 fix would need a CRD update path first.


### 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 (walked the
map: the diff changes a template, a test, and the wording of two
descriptions; no field, default, enum or name changes)
- [ ] [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(redis): an instance with `replicas: 1` now runs one Sentinel instead of three, without a Sentinel PodDisruptionBudget; on upgrade, the PDB the operator created earlier is deleted. Instances with two replicas or more are unchanged.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Redis deployments configured with one replica now use one Sentinel;
other replica settings continue to use three.
* Workload monitoring now reflects the configured Sentinel count. When
changing to one replica, upgrades remove the Sentinel
PodDisruptionBudget.
* **Documentation**
* Updated Redis configuration guidance to explain how Sentinel count
varies with the replica setting and clarify Sentinel resource defaults.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:17:32 +03:00
Aleksei Sviridkin d61db784a6 fix(mariadb-operator): derive the cluster name from the platform domain (#5123)
<!-- 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

Fixes #2348.

`packages/system/mariadb-operator/values.yaml` pins
`mariadb-operator.clusterName: cozy.local`, and the vendored chart turns
it into the operator's `CLUSTER_NAME` and into a SAN of the webhook
certificate. On a platform installed with any other
`networking.clusterDomain`, the operator resolves its own pods as
`<name>.<ns>.svc.cozy.local` and gets NXDOMAIN. Observed on a cluster
with `clusterDomain: lab.local`: a 2-replica MariaDB never gets
replication configured (`status.replication` stays `{}`, the secondary
runs as a standalone semi-sync master), and User, Database and Grant
fail with `lookup mariadb-<name>-primary.<ns>.svc.cozy.local ... no such
host`, while the operator only retries at verbosity 1.

`clusterName` is a value of the vendored subchart, which the `_cluster`
lookup used by package templates cannot reach. The platform now carries
`networking.clusterDomain` under the `mariadb-operator` subchart key in
the shared `cozystack-values` Secret, the same route
`keda.clusterDomain` and `opensearch-operator.manager.dnsBase` already
take. The literal in the package stays as the fallback for a render
without that Secret, so a cluster on `cozy.local` renders exactly as
before.

Tests cover both ends of the path:
`packages/core/platform/tests/apps_mariadb_operator_cluster_name_test.yaml`
pins that the Secret carries the domain under the anchored subchart key
and tracks a non-default value (it fails with the `apps.yaml` change
reverted), and
`packages/system/mariadb-operator/tests/cluster_name_test.yaml` pins
that a domain set on that key reaches the operator's `CLUSTER_NAME` and
the webhook certificate, with `cozy.local` kept as the standalone
default. `make helm-unit-tests` passes locally.


### 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
fix(mariadb-operator): the MariaDB operator now uses the platform's networking.clusterDomain instead of a hard-coded cozy.local, so replication and User/Database/Grant reconciliation work on clusters with a non-default cluster domain.
```


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

## Summary by CodeRabbit

* **New Features**
* MariaDB now uses the configured networking domain as its cluster name
and webhook certificate address. If no custom domain is configured, it
continues to use the default cluster name and address.
* **Tests**
* Added coverage for default and custom domain settings, including their
effect on the MariaDB cluster name and webhook certificate address.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:07:37 +03:00
Aleksei Sviridkin 0c486263f9 fix(postgres): render the declared databases in a stable order (#5091)
<!-- 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 postgres init script lists the declared databases with `keys
.Values.databases`, which returns them in Go's randomized map order. A
release with two or more databases therefore renders a different
`DEFINED_DBS` line on each upgrade, so the `<release>-init-script`
Secret and the `checksum/config` annotation of the init Job change on
upgrades that did not touch the databases. Sorting the keys makes the
render a function of the values alone.

Found while measuring a platform-wide values change on a production
cluster (#5092): of 36 releases that ran Jobs again, 31 rendered a
byte-identical manifest across the two Helm revisions and the other 5
were Postgres releases whose only difference was this ordering, e.g.
`DEFINED_DBS="db_a db_b"` → `DEFINED_DBS="db_b db_a"`.

This does not stop the init Job from running on every upgrade (it is a
`post-upgrade` hook); the issue covers that part.

Verification: the new `tests/init_script_deterministic_test.yaml`
renders eight databases declared out of order and expects them sorted.
Without the fix it failed 5 runs out of 5 with a different order each
time; with it, `helm unittest packages/apps/postgres` passes (27 suites,
210 tests).


### 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
fix(postgres): the init script renders the declared databases in a stable order, so an upgrade that leaves the databases unchanged no longer rewrites the init-script Secret.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Database initialization now lists configured databases in alphabetical
order, providing consistent output across deployments.
* **Tests**
  * Added coverage to verify the alphabetical ordering.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:06:40 +03:00
Aleksei Sviridkin 72ff22dc9e fix(securitygroups): project a membership-only group to a policy Cilium accepts (#5133)
<!-- 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

Fixes #5129.

A SecurityGroup with `attachments` and no rules still projects to a
backing CiliumNetworkPolicy, since that policy carries the attachments
the securitygroup-controller reconciles. Its spec was `endpointSelector`
plus `ingress: []`. The CRD `anyOf` accepts that, but the agent's
`api.Rule.Sanitize` counts entries (`len(r.Ingress) == 0 && ...`, the
same in v1.19.5 and v1.20.2), so Cilium marks the policy `Valid=False`
with "rule must have at least one of Ingress, IngressDeny, Egress,
EgressDeny". `pkg/apis/sdn/DESIGN.md` §5 claimed the opposite.

The projection now writes one empty ingress rule, which allows nothing,
plus `enableDefaultDeny: {ingress: false, egress: false}`, so that rule
does not put the member pods into default deny. The override relies on
the agent's `enable-non-default-deny-policies`, which defaults to `true`
in the vendored chart; DESIGN.md says so. `ingress` is now `omitempty`
like `egress`, because an empty list no longer serves any purpose. The
reverse projection hides that exact spec, so the group still reads back
with no rules. `enableDefaultDeny` is what tells it apart from a
tenant-authored empty ingress rule, which keeps its default deny and
stays visible.

`TestRulesLessSecurityGroupProjectsValidPolicy` now checks the wire form
against the Sanitize rule and fails on `main` (`ingress:[]`).
`TestEmptyIngressRuleIsNotMembershipOnly` covers the tenant-authored
empty rule. `go test ./pkg/registry/sdn/...` passes.

Checked on a lab running Cozystack v1.6.5 with Cilium 1.19.5
(`enable-non-default-deny-policies: true`), in a throw-away namespace
with no baseline policy. A membership-only group `front` attached to
`web`, and a group `db` on `srv` allowing `fromSG: [front]` on 8080. The
v1.6.5 projection gives `front` as `VALID=False`. With the spec this
branch writes for `front` applied to its CNP, it turns `VALID=True`
("Policy validation succeeded"), and every probe is unchanged: `web ->
srv:8080` OK, `web -> srv:9090` and `other -> srv:8080` refused, `other
-> web:8080` and `web -> other:8080` OK, so the placeholder isolates
nothing. Moving the attachment of `front` from `web` to `other` flips
the `fromSG` peer: `other -> srv:8080` OK, `web -> srv:8080` refused.

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

The diff touches `pkg/registry/sdn/securitygroup/` and
`pkg/apis/sdn/DESIGN.md` only: no package, chart, CRD, `hack/` or
convention change. The website mentions SecurityGroups only in the v1.6
blog post and the PCI-DSS page, neither of which describes the backing
policy.

<!--
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
fix(securitygroups): a SecurityGroup with attachments and no rules no longer leaves an invalid CiliumNetworkPolicy behind; its backing policy is now accepted by Cilium and still allows or denies nothing.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Membership-only SecurityGroups now round-trip without displaying
ingress or egress rules, while remaining usable without triggering
default-deny behavior.
* SecurityGroups with an explicitly configured empty ingress rule
continue to retain that rule when read back.
* This behavior depends on Cilium’s `enable-non-default-deny-policies`
option being enabled; when disabled, Cilium applies default deny to
every policy.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:05:32 +03:00
Aleksei Sviridkin 9e8e10c307 fix(backups): re-render the default backup objects when the S3 endpoint changes (#5230)
## What this PR does

Refs #5205.

The S3 endpoint the platform backups use is copied twice, and neither
copy is refreshed after `publishing.host` changes. The COSI sidecar
writes `BucketInfo` once per `BucketAccess`, so the credentials Secret
`bucket-cozy-backups-system-credentials` rendered from it keeps the old
endpoint. The Velero BSL and the default Strategy CRs keep whatever
endpoint the last render of `backupstrategy-controller` saw. The default
objects gate only acted on missing objects, so backups kept going to the
old host and nothing reported it.

The gate now also forces a render when a copy is stale:

- the bucket's `<bucket>-system` release, when the credentials Secret's
endpoint differs from the COSI Secret it is rendered from;
- `backupstrategy-controller`, when the endpoint an object was rendered
with differs from the credentials Secret's. The chart stamps the
rendered endpoint on the BSL and on every default Strategy as
`backups.cozystack.io/endpoint`, because the endpoint itself sits at a
different path in every kind. Objects without the annotation count as
stale until a render stamps it; `MinForceInterval` bounds the forces.

Stale credentials are forced before the BackupClass and the routed
objects are read, so an absent BackupClass or an unreadable Strategy
still fails the check but does not keep the old endpoint in the
credentials Secret. A `BucketInfo` without an endpoint is not compared,
since rendering from it would write an empty endpoint or fail the bucket
release. Repairing missing objects keeps working while the credentials
comparison is stale or fails, and a `systemSecretName` that is not
derived from a COSI Secret (no `-credentials` suffix, or the raw COSI
Secret itself) is not compared at all. RBAC is unchanged: the controller
already holds cluster-wide `get` on Secrets. No new metric: nothing
would consume it.

What this does not do: the COSI Secret still holds the endpoint the
`BucketAccess` was granted with, so after a host change the endpoint
follows only once an operator deletes the `BucketAccess` and forces
`bucket-cozy-backups` to recreate it (this issues new S3 keys). From
there the gate replaces the remaining manual steps of the recovery
described in the issue. Automatic re-grant is left for a separate issue,
and the operator runbook goes to the website, hence `Refs` rather than
`Fixes`.

Known limitation: the credentials comparison runs only when
`backupStorage.systemSecretName` is
`bucket-<bucketName>-system-credentials`, the Secret the forced
`bucket-<bucketName>-system` release renders. If an operator sets
`bucketName` and `systemSecretName` inconsistently, the comparison is
switched off rather than forcing a release that never rewrites the
compared Secret, so a stale endpoint in that Secret is not repaired.
Repairing a missing credentials Secret still forces the release derived
from `bucketName`, as before this PR.

Tested with unit tests for the gate (including the comparison
normalisation shared with the chart helpers), helm-unittest for the
annotation and the wiring, and a bats guard that every rendered Strategy
CR and the BSL carry the annotation. On a live cluster, a server-side
render of the chart produced exactly the annotation the gate derives
from the real credentials Secret, so the steady state does not force
anything.

### Screenshots

Not a UI change.

### Downstream repositories

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up: an operator runbook for changing `publishing.host` with
platform backups enabled (re-grant the `BucketAccess`, then let the gate
converge; restart ClickHouse pods that read the credentials from
environment variables). Not filed yet, pending approval of the text.
- [ ]
[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(backups): the backup controller now re-renders the Velero BSL, the default backup strategies and the platform bucket's credentials Secret when their S3 endpoint is stale, so after the platform bucket's BucketAccess is re-granted for a new publishing.host, backups move to the new endpoint without forcing releases by hand.
```


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

## Summary by CodeRabbit

* **New Features**
* Backup strategies and storage locations now record their storage
endpoint, enabling detection and repair of outdated endpoint settings
after bucket access changes.
* Bucket credentials and backup objects are refreshed independently when
their endpoints are missing or out of date.
* **Documentation**
* Clarified when endpoint changes trigger updates and how suspended
releases are handled.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-09 12:02:42 +03:00
myasnikovdaniil 7097cc5cda feat(release): make backports automatic with backport-batch (#5106)
## What this PR does

Backports become automatic. Fix merged to `main` with `fix` title, or
`security` for fix that comes through security advisory, reaches release
lines through batch that runs on Monday and Thursday, newest line first,
nobody requests, labels or opens backports by hand. Batch runs for two
newest lines with stable release (`released-lines` in policy block) and
every line branched after them that has no stable release yet: with 1.6
and 1.7 released, `release-1.8` created at `v1.8.0-rc.1` joins them, and
`v1.8.0` drops 1.6. Line counts as released once `vX.Y.Z` tag without
pre-release suffix exists on origin. Rules are in
`docs/release/backports.md`, `cmd/backport-batch` applies them,
`.github/workflows/backport-batch.yaml` runs it. Design is in
cozystack/community#114, merge shape of each original in
cozystack/community#119.

- `backport-batch run --line release-X.Y` takes PRs merged to `main`
since line branched (title and body read from merge commit) and skips
what rules exclude: other types and `deps`, breaking changes, platform
migrations, `targetVersion`, changed CRD documents, API changes that API
review gate calls sizeable (batch runs `internal/apigate` itself
whenever PR changes a file the gate parses:
`pkg/apiserver/apiserver.go`, an ApplicationDefinition, or YAML under
`packages/` or `internal/` naming `CustomResourceDefinition` or
`APIService`), and on older line what next newer line doesn't carry yet.
The rest goes onto `backport-batch/<line>-<date>` in merge order. PR
merged to `main` with merge commit goes as its own commits picked with
`cherry-pick -x --signoff`, so each keeps its author, message and
sign-offs and gets sign-off of whoever runs the batch (with `-m 1`
author was whoever merged on `main`), merges of `main` into PR branch
are left out, and merge commit joins them to the batch with original's
merge message and `Backported-from:` trailer. Squash-merged PR goes as
one pick. Conflict on any commit takes branch back to where it was
before that original, it's listed with the commit that failed, run goes
on. `--push` opens or updates draft PR, `--continue` carries on open
batch PR as fast-forward only, PR body it didn't write is never
rewritten. Same-day run over batch branch whose PR never got opened
refuses and names `run --line <line> --onto <head> --push`, which
continues it. Run or `--defer` that resets local batch branch over
commits only it has prints `git branch` command that restores them.
- `run --defer <N> --reason <why>` takes original out of open batch
(usually the one failing CI is pinned on). It rebuilds branch replaying
batch unit by unit without it, so merges and pushed conflict resolutions
keep their shape, and only this rebuild pushes with
`--force-with-lease`. Deferred original comes back in next batch.
- Line carries something only if its own history shows it: picks whose
`-x` line, the one git appends, names PR's merge commit or every commit
of its own, or pick that changes nothing. Picked commits keep their
messages and batch merge carries PR body, so text in them (`-x` lines,
`Backported-from:`, `Backport of #N`) counts only on commits from before
`batch-only` day in policy block, where it records old hand backports
and batches. Otherwise text in a merged fix could mark another fix as
already backported, and it would never reach the line. PR against line
never counts. Open batch PR counts only from this repo and from author
with write access or the batch's own bot (`BACKPORT_BATCH_BOT`, exact
login, `cozystack-ci[bot]` when unset, so maintainer's run by hand
continues scheduled batch instead of opening second one), and is read
the same way. Actions bot isn't trusted anymore, any workflow token
writes as it.
- Any failed GitHub API or git call stops the run with message naming
the call, no retries. Failed permission lookup used to count batch PR
out as look-alike, so `--continue` opened second batch PR and `notify`
posted second comment; now `run`, `check`, `verify` and `notify` exit 2
and write nothing.
- Policy applies fully from `release-1.7`. `release-1.6` takes only
fixes merged from 2026-10-26, older ones need `backport/include`. Start
days are in `backport-policy` block on the policy page, test keeps page
and code in sync.
- `backport-batch check --cut` replaces `backport-audit` as gate before
patch cut and before every rc after the first, it also fails while a fix
is only on open batch PR. Plain `check`, which scheduled run uses, lets
those through. `backport-batch fold` checks conflict resolution in the
same shape: merge on batch head whose second parent is straight line of
picks of PR's own commits in order, each with its `-x`, only PR's files
(one pick for squash). `backport-batch verify` holds every original on
batch to that shape, clean picks included, and `pr-labeler.yaml` runs it
on every push to batch PR: `pull_request_target` so it reaches
`release-*`, tool built from `main`, batch head fetched as data,
read-only token. Batch PR body says per original how many commits it
brought and which picks differ from their commits. `backport-batch
notify` reports batch that is still open from previous run and deferred
conflicts in one comment on batch PR, edited in place, with
`BACKPORT_ALERT_TEAM` mention.
- `hack/backport-batch-scheduled.sh` is what the workflow runs and
maintainer can run by hand (`--dry-run` changes nothing on GitHub): for
each line newest first `run --continue --push`, `check`, `notify`. It
pushes with `BACKPORT_BATCH_TOKEN` only and fails without it, never
`GITHUB_TOKEN`, because its pushes don't start CI. Workflow runs it as
cozystack-ci App: it mints token for this repo only with Contents and
Pull requests write, passes it as `BACKPORT_BATCH_TOKEN`, commits as
`cozystack-ci[bot]`, and sets `BACKPORT_BATCH_BOT` so the tool trusts
that bot's batch PR and comment. Job timeout is 30 minutes, under the
token's hour.
- App has no workflows permission, so workflow runs the script with
`--no-workflow-files`: original that changes `.github/workflows/` isn't
picked and waits on batch PR for maintainer, listed with its workflow
files in body and in notify comment. It's never a conflict. `check`
warns about it while it waits and `check --cut` fails on it. Maintainer
brings it over with own token by `run --continue --push` without the
flag, or takes it out with `--defer`.
- Deferred conflicts and failed batch CI wait for maintainer. `notify`
keeps one comment on batch PR listing stalled batch, conflicts and
workflow changes, mentioning `BACKPORT_ALERT_TEAM` (team or login) when
it's set. Failed batch CI isn't in the comment, it shows on batch PR
checks, and `check --cut` fails while batch is unmerged.
- Two steps that need judgement (resolve conflict, pin failed CI on
original and defer it) are written as runbooks in
`docs/release/backport-tasks/`: inputs, the only allowed output, check
it must pass before push, what never to do.
- `backport/include` and `backport/skip` let maintainer override rules
for single PR, they are new `backport/` category in `labels.yml`. Skip
wins over include, if open batch already picked it `--defer` takes it
out.
- Text written by other people (titles, release notes, reasons, file
names) goes to batch PR body only inside code spans or release-note
block, control, bidi and zero-width characters are dropped, file names
in deferred block included, git args go after `--end-of-options`, no
shell.
- Removed: `cmd/backport-audit`, `backport.yaml`, `kind/backport` and
`kind/backport-previous` labels with aliases and stale exemption,
trailer-check exemption for bot branches. `Commit trailers` job exempts
only `backport-batch/*` branches of this repo, changelog instructions
read originals from batch commits. Exemption lives in main's
`pre-commit.yml`, and batch PR runs its line's copy: `release-1.7` has
its own copy that exempts only old `backport-<pr>-to-<branch>` branches,
`release-1.6` has no such job. So on `release-1.7` picked commit with
bad trailer fails batch CI until line gets main's copy.
- `hack/close-manual-backport.sh` runs from `pr-labeler.yaml` on
`opened` and closes PR against `release-*` from outside contributor
(association contributor, first-time contributor or none, permission
read or none). Owners, members, collaborators, anyone with triage or
more and bots are left alone. When GitHub's answer on author's
permission can't be read, it closes nothing and fails the job naming the
failed read. Maintainer keeps release-only fix by reopening it.
Release-only PRs are occasional and opened by maintainers, for fix with
no counterpart on `main`, or one that on `main` came inside another PR
or was written for `main`; if its counterpart on `main` is a fix the
batch would take, the batch skips it once picking it changes nothing on
the line, otherwise maintainer labels it `backport/skip`.
- `docs/release.md` is split into pages under `docs/release/`, PR
template doesn't ask for `kind/backport` anymore. `security` type is
added to commit format in `docs/agents/contributing.md`, PR template,
Gemini review guide and labeler's list (labeler maps no `kind/*` to it,
`labels.yml` has no fitting one).
- `SECURITY.md`, `docs/security/incident-response.md` and
`GENERAL_TECHNICAL_REVIEW.md` now say two latest minor releases are
supported, same window the batch runs for, and link the backport policy
for which fixes go there.

Dry runs without push, on 2026-11-06, with `--no-workflow-files` as the
workflow runs it: `lines` gives `release-1.7` and `release-1.6`. On
`release-1.7` batch picks 12 fixes, defers 3 conflicts (#4756, #4867,
#5082), leaves #4888 and #4971 to a maintainer because they change
workflows, and skips 49. On `release-1.6` it picks #5088, defers 3
conflicts (#5071, #5086, #5087), leaves #5142 to a maintainer, and skips
590. `verify` passes both batches.

Setup after merge: no new secret. Batch runs as cozystack-ci App from
existing `COZYSTACK_CI_APP_ID` and `COZYSTACK_CI_PRIVATE_KEY`, job fails
when either is missing. App's installation already has Contents and Pull
requests write on all org repos, and no Workflows is needed: PRs that
change `.github/workflows/` wait for maintainer, who brings them over
with own token as "Changes to workflows" in `docs/release/backports.md`
says. Today that's #4888 and #4971 on `release-1.7` and #5142 on
`release-1.6`. Set `BACKPORT_ALERT_TEAM` to a team or a login (for
example `cozystack/maintainers`): unset, batch comment mentions nobody
and nobody is notified. Once this PR is merged, `Backport batch shape`
can be required check in the release lines ruleset (`release-*`
excluding `release-*.*.*`), with "Do not require status checks on
creation" on, because `cut-prerelease.yaml` creates `release-X.Y` by
push. `PR Auto-Label` with both new jobs runs on `pull_request_target`,
so it needs #5135 sorted before 2026-12-02. `kind/backport*` labels have
to be deleted by hand, label sync doesn't delete.

### Screenshots

Not a UI change.

### Downstream repositories

cozystack/community `contributors/README.md` lists commit types, so it
needs `security` too, it has no backport label text. Website has no
backport label instructions, and its links to `docs/release.md` carry no
anchor, the page stays as index. Trigger map lists commit convention
change for cozystack/ccp: this adds `security` type, but not message
format or trailer, and `package-bump` writes only `chore(...)` commits
and restates the trailer, so nothing changes there.

- [ ] 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: add `security` to commit types in `contributors/README.md`

### Release note

```release-note
feat(release): backports are automatic. Fixes merged to main with fix or security title reach release lines through a batch built by cmd/backport-batch every Monday and Thursday, by the rules in docs/release/backports.md. The batch runs for the two newest lines with a stable release and for a line in its rc window: the release-X.Y branch the first rc creates joins at once, and its vX.Y.0 drops the oldest line. Features, chores, docs, dependency bumps, breaking changes, migrations, CRDs and API changes that the API review gate calls sizeable wait for the next minor. The policy applies fully from release-1.7, release-1.6 takes only fixes merged from 2026-10-26. Each original reaches the line as its own commits under a merge commit, keeping their authors and sign-offs. Maintainers override the rules for single PR with backport/include and backport/skip labels and take an original out of a batch with backport-batch run --defer. backport-batch check replaces backport-audit as the gate before a patch release and before a later rc. The label-driven backport workflow and kind/backport labels are removed. Backport PR opened by hand against a release branch by author with read access or less is closed automatically. The scheduled batch runs as the cozystack-ci App and needs no secret of its own; a fix that changes .github/workflows waits on the batch pull request for a maintainer, since the App may not push workflow changes. Release documentation now lives in one page per topic under docs/release/.
```
2026-11-09 09:42:38 +05:00
Mattia Eleuteri 9fba8af69c test(redis): pin the release name in the Sentinel PDB hook for the longest release
The long-name case checked only that the Job name fits in 63 characters and that the Job targets the right PDB. A Job name that dropped the release, letting two releases' hooks replace each other, or a Role granting a different PDB would both have passed.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-11-08 19:45:18 +01:00
Marian Koreniuk e92bc8664b fix(tests): keep two unit tests green after the release image restamp
The release build rewrites packages/*/values.yaml: the kube-ovn image tag
becomes the Cozystack version and the traces collector becomes the
first-party otelcol-tracing build. Two unit tests read exactly those
fields, so every PR against release-1.7 has failed Unit & controller
tests since the first rc, and release-1.8 would repeat it.

Same change as #5179 on release-1.7.

Assisted-by: LLM
Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
2026-11-08 19:37:03 +01:00
Mattia Eleuteri a7f84242d1 fix(redis): keep three Sentinels for a zero or negative replica count
The operator runs three Redis pods when the count is zero or negative, but the chart treated any count up to 1 as the single-pod case. Such a cluster got one Sentinel and no Sentinel PDB, while three Redis pods ran under it.

Assisted-by: LLM
Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
2026-11-08 19:36:44 +01:00
Alexey Artamonov c6c107fd08 docs(backups): state when the gate compares endpoints in the values reference
The values reference read as if the credentials endpoint were always
compared and an object without the endpoint annotation never forced a
render. Operators sizing the force counter or choosing a custom
systemSecretName need both conditions spelled out.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov ee5e51ee6c fix(backups): keep forcing objects that still lack the endpoint annotation
An object without backups.cozystack.io/endpoint stopped counting as
stale as soon as one forced render had been requested, whether or not
that render succeeded. A failed upgrade left the objects unannotated
and the gate silent until the controller restarted, which also hid the
climbing force counter meant to report exactly that failure. Such an
object now stays stale until a render stamps it; MinForceInterval
already bounds the cost.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov dc909c778c fix(backups): compare the credentials endpoint only for the forced bucket's Secret
The gate forces bucket-<bucketName>-system, which renders the
credentials Secret of that bucket's system user and nothing else. The
COSI Secret name was derived from systemSecretName instead, so a value
naming another bucket's Secret made a lagging endpoint there force the
platform bucket release every throttle window without ever refreshing
the Secret that was compared.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov bd2b6c4b49 fix(backups): leave the credentials Secret alone when BucketInfo has no endpoint
A BucketInfo without an endpoint differs from every credentials Secret
that has one, so the gate forced the bucket release on every throttle
window. That render cannot converge: an empty endpoint would be written
into the Secret every driver reads, and with the key absent the
template's trimPrefix fails and parks the release in Failed.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov 399ccd3f4a fix(backups): refresh stale credentials even when the routed objects are unreadable
The credentials endpoint comparison needs only the credentials Secret and
the COSI Secret it is rendered from, yet its forced render was issued only
after the BackupClass and every routed object had been read. A missing
BackupClass or one unreadable Strategy therefore kept the old endpoint in
the credentials Secret, which the projector hands to every driver, not
just the one whose object failed.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov ef7626a951 fix(backups): keep repairing missing default objects while credentials lag
The credentials comparison returned early when the credentials Secret
was stale or the COSI Secret could not be read, so a missing default
Strategy or BSL was no longer created on that tick, nor on any later
one while the bucket release stayed suspended, kept failing, or the
COSI Secret stayed unreadable. Repairing missing objects is what the
gate did before the comparison existed, and it does not depend on the
answer.

Run the object check on every tick and force each release for what it
renders, reporting a comparison error alongside instead of in place of
the repair.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Alexey Artamonov d0f47f7dd5 fix(backups): compare the credentials endpoint only for a Secret derived from COSI
backupStorage.systemSecretName may name a Secret the bucket chart does
not render from a COSI Secret: the raw COSI Secret itself, which the
projector accepts, or any Secret without the -credentials suffix. The
COSI Secret name was then the source name unchanged, so the gate
compared a Secret with itself and either reported it stale on every
tick or failed the check, and stopped repairing missing default objects
as well. Skip the comparison when there is no distinct COSI Secret or
no endpoint key to compare.

The gate compares the annotation with what the chart's endpoint
helper renders, and the helper renders a present but blank endpoint as
a bare https:// instead of falling back, so the gate has to compute
the same value or its expectation and the chart disagree for that
Secret. The gate's docs, metric help and log lines named only missing
objects, so they did not cover a stale endpoint they now report.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-08 20:18:29 +03:00
Kirill Ilin cb3fcd3357 feat(operator): let the platform source trust a private registry CA (#5237)
## What this PR does

The operator creates the `cozystack-platform` OCIRepository from its own
command-line arguments and rewrites the whole object on every start;
`cozystack-packages` copies that spec. When the platform source is a
registry whose certificate is issued by a private CA (an internal
pull-through cache in an air-gapped install, for example), there is no
durable way to make Flux trust it: a `certSecretRef` patched onto the
live object is dropped on the next operator restart, source-controller
then fails TLS, and platform updates silently stop arriving.

This adds `--platform-source-cert-secret` to the operator and
`cozystackOperator.platformSourceCertSecret` to the installer chart. The
named Secret in `cozy-system` is set as `spec.certSecretRef` on the
generated OCIRepository, so it holds `ca.crt` for a private CA and/or
`tls.crt`+`tls.key` for client certificate auth, with the Flux key
layout. `cozystack-packages` inherits it through the existing spec copy.

The flag is OCI only. `GitRepository` has no `certSecretRef` (Flux reads
a Git CA from the `ca.crt` key of the `secretRef` Secret), so a Git
platform source with the flag set is refused at startup instead of
having its CA silently ignored.

Tests: unit tests for the generated spec, a regression test that the CA
survives the operator's update of an existing OCIRepository, a test that
a Git source with the flag writes nothing, and a helm-unittest suite for
the new chart value.

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

- [ ] No downstream repository is affected by this change
- [x] [cozystack/website](https://github.com/cozystack/website) -
follow-up: https://github.com/cozystack/website/pull/791
- [ ]
[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 new installer value is optional, so `ansible-cozystack` is not
reached. The website's air-gapped guide
(`content/en/docs/next/install/kubernetes/air-gapped.md`) added the
private CA to the nodes only, while Flux fetches the platform source
itself; cozystack/website#791 adds a step for `platformSourceUrl`,
`platformSourceSecret` and `platformSourceCertSecret`.

### Release note

```release-note
feat(operator): the platform source can be served from a registry behind a private CA. Set `cozystackOperator.platformSourceCertSecret` in the installer to a Secret in `cozy-system` with `ca.crt` (and optionally `tls.crt`/`tls.key`); the operator sets it as `certSecretRef` on the `cozystack-platform` OCIRepository, so it survives operator restarts.
```


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

## Summary by CodeRabbit

* **New Features**
* Added the `platformSourceCertSecret` setting for OCI platform sources.
It allows a Secret containing certificates for private CA trust or
client authentication to be configured separately from registry
credentials.
* The configured certificate Secret is applied to the OCI source and
inherited by its package source.
* **Configuration**
* The setting is empty by default. Git platform sources do not support a
separate certificate Secret; use the existing platform source Secret for
Git TLS certificates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-06 22:21:19 +05:00
Kirill Ilin 0a448963e3 docs(installer): say a CA-only cert secret must be Opaque
kubernetes.io/tls requires both tls.crt and tls.key, so a Secret that
carries only ca.crt is rejected under that type.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-06 18:29:18 +05:00
Kirill Ilin fbfc270c71 test(installer): tie each platform source flag to its own value
Both secrets were only ever set together, so a cert flag keyed on
platformSourceSecret stayed green. A pull-through cache behind a private
CA usually needs the CA without credentials, which is exactly the case
such a slip breaks. The absence checks match the flag by prefix, since
a whole-element comparison misses the flag rendered with any value.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-06 18:29:18 +05:00
Kirill Ilin dfd7cbed36 fix(operator): reject a Git cert secret before installing CRDs and Flux
CRDs and Flux are installed before the platform source, so refusing a
cert secret on a Git source only when the source is written exits the
operator after a partial install. The check now runs right after flag
parsing, and the source install reuses it.

The guard runs on every start, so a Git source without a cert secret is
now covered over https and ssh: a guard that misfired there would
crash-loop the operator for every Git platform source. The update test
also renames the cert Secret, as a CA rotation does.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-06 18:28:45 +05:00
Myasnikov Daniil 1ceaeb8b6e docs(security): support the two latest minor releases
The security policy promised three supported lines and said no rule
decides which of them a fix reaches. The backport batch runs for the
two newest lines with a stable release, by the rules on the backport
policy page, and the maintainers agreed to make that the support
window. SECURITY.md, the incident-response summary and the technical
review now name the two latest minor releases, say that the older one
drops out when a new minor ships, and send a reader to the backport
policy for which fixes reach them.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:37 +00:00
Myasnikov Daniil eaa0169ff3 chore(release): retire the backport bot, its labels and backport-audit
The batch replaces the label-driven backport workflow and the audit
that gated a cut against it. The kind/backport labels, the stale
exemption for them, their contract tests and the audit's make target
go with them. labels.yml gains a backport/ category instead, with
backport/include and backport/skip, the overrides the batch reads off
a merged pull request.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:37 +00:00
Myasnikov Daniil 4a750a1e23 ci(release): run the backport batch twice a week
The batch runs on Monday and Thursday from a script a maintainer can
also run by hand. It runs as the cozystack-ci App with a token for this
repository alone and Contents and Pull requests write, so its pushes
start CI on the batch and no person's token is involved. The App may
not push a workflow change, so such an original waits for a
maintainer.

pr-labeler holds every batch pull request to the batch's shape, with
the tool built from main and the head read as data. It also closes a
pull request into a release line opened by someone with read access
or none, since fixes reach the lines through the batch. The Commit
trailers check exempts a batch branch of this repository, whose picked
messages cannot be rewritten. Promotion now says how to bring its own
pipeline to a line through the batch.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:37 +00:00
Myasnikov Daniil 9f8c88b4f1 feat(release): build the backport batch with backport-batch
The rules need a tool that applies them the same way on every run and
shows what it did. backport-batch reads main's first-parent history,
judges each pull request by the policy block it reads from the page on
main, and picks what qualifies onto one batch branch per line. Each
original lands as its own commits under a merge that names it, so
authors and sign-offs survive and the line's own history shows what it
carries.

check is the gate before a cut. fold holds a hand resolution of a
conflict to the batch's shape, verify holds a whole batch to it, and
notify reports what waits on the batch for a maintainer.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:36 +00:00
Myasnikov Daniil c12dcdddb4 docs(release): make backports automatic, by written rules
Backports were asked for pull request by pull request with labels and
opened by a bot, so whether a fix reached a release line depended on
someone remembering to ask, and the lines drifted apart. The policy
page states the rules instead: a fix or security fix merged into main
goes to every line the batch runs for, unless it is breaking, touches
platform migrations or a CRD, or changes the API in a way the API
review gate calls sizeable. Two labels let a maintainer override the
rules for one pull request.

The task pages give a maintainer the steps for a deferred conflict and
for a failed batch CI. The backports route in AGENTS.md, the
contributing and changelog guides and the PR template point at the
policy, and the commit format lists the security type the policy
takes.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-06 12:33:35 +00:00
myasnikovdaniil 970191dc8a fix(clickhouse): restore the archive a Backup was taken as (#5208)
## What this PR does

A ClickHouse `RestoreJob` restored the newest archive under the app
prefix, whichever `Backup` it named. Strategy pod generated the archive
name with `date -u` and nothing was recorded on the `Backup`, so restore
could only list the remote archives and take the last one. Restoring an
older `Backup` brought back the latest data.

Now the controller names the archive before it creates the backup Job
(`clickhouse-<app>-<startedAt>` from persisted `status.startedAt`, so
retried pods write the same one), passes it to the pod as `.ArchiveName`
and records it in the `Backup` `driverMetadata`. Restore takes exactly
that archive and fails with its name when storage has no such archive.
The name is recorded only when the Job's pod got it, so a Job left from
the previous controller during an upgrade, or a custom strategy with its
own names, leaves no name instead of a wrong one.

A retried pod reuses the name, so it waits for an attempt the sidecar
still runs (no 423) and asks for `resume`.

A `Backup` outlives its `BackupJob` and a Plan names `BackupJob`s after
the schedule slot, so a `BackupJob` created again under a used name met
the earlier run's `Backup` when its Job completed, took it as its result
and left the archive it just wrote recorded nowhere. Now it fails if
that `Backup` records another archive, and the message names both. A
retry of the same run still gets its own `Backup` back.

Old `Backup`s have no recorded name and stay restorable: the pod logs a
`WARNING` and takes the newest archive stamped no later than `takenAt`,
and fails when nothing qualifies. While the chart rolls out, previous
controller pod can still render the new template and pass no `takenAt`,
so a restore of a `Backup` without recorded name fails before it touches
any table, and works when created again after the rollout.

Fixes https://github.com/cozystack/cozystack/issues/5199

Tests are go unit tests for naming, recording, a reused `BackupJob` name
and restore render, and `hack/altinity-backup-strategy.bats`, which runs
the shipped script against a stubbed clickhouse-backup API (the restore
tests go red on the old template). Checked on a live ClickHouse with two
backups of 1500 and 2000 rows: restore of the older one by recorded name
ends with 1500 rows, fallback for an unnamed older backup picks the same
archive with the warning, a missing archive fails without touching the
table, a retry over an existing local backup completes under the same
name. The controller binary itself was not run on the cluster, only its
rendered output in the pod.

### 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(clickhouse): a RestoreJob now restores the archive its Backup was taken as, not the newest one. New Backups record the archive name, Backups taken before this release are restored best effort from the newest archive stamped no later than their takenAt, with a warning in the restore pod log. A ClickHouse BackupJob created again under a name whose Backup still exists now fails instead of reporting that Backup as its result.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Backups now use a consistent archive name tied to the backup job, and
completed backups retain that name for future restores.
* Restores can select the recorded archive and backup timestamp. If no
archive was recorded, the system selects the newest eligible archive for
the application.
* **Bug Fixes**
* A restore now fails when its recorded archive is missing instead of
silently selecting a different one.
* Backup requests avoid starting a duplicate action when the matching
action is already in progress.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-06 11:54:38 +05:00
myasnikovdaniil 18779dddfc fix(backups): report sizeBytes for MariaDB and CNPG backup artifacts (#5223)
## What this PR does

Fixes https://github.com/cozystack/cozystack/issues/4316.

Continues https://github.com/cozystack/cozystack/pull/5015 by androndo
on a separate branch. All ten of their commits are kept as they were
(authorship, messages and order, only rebased on current `main`).
MariaDB and CNPG backups stored in S3 report
`Backup.status.artifact.sizeBytes`, the size of the dump
mariadb-operator uploaded or the total of the barman base backup, and
the field stays unset when the size cannot be attributed to that one
backup.

Four commits are added on top:

- `test(backups)`: operator Backup fixtures carry the condition
mariadb-operator writes on success, `Complete=True` with reason
`JobComplete` and message `Success`. Operator sets `Complete=True` for a
failed Job too (reason `JobFailed`), so a fixture without reason
describes a state that never happens.
https://github.com/cozystack/cozystack/pull/5207 makes the driver check
that reason, the tests pass with and without it.
- `refactor(backups)`: the MariaDB matcher returns the matched dump
(key, file name, timestamp from the name, size, LastModified) and tells
"not found" from "ambiguous", and the Secret lookup and S3 client live
on a small value any reconciler can build from its own client. A MariaDB
restore needs this to restore the dump of the Backup it was given and
not the newest one under the prefix. `sizeBytes` does not change here.
- `fix(backups)`: the probe reads bucket, prefix, endpoint, credentials
and TLS from the operator Backup's own `spec.storage.s3`, not from the
strategy as it renders now, so editing the strategy during a backup does
not send the probe somewhere else.
- `fix(backups)`: a dump is attributed only when its ownership is
proven. Before, a backup pod with a clock more than a minute behind,
right after a previous run, got that run's dump size (666 bytes for a
123-byte dump in a test). Now the dump has to be the only one that
either clock places in the operator Backup's lifetime, both clocks have
to agree, and neither may place it in the lifetime of another finished
operator Backup with the same bucket and prefix. Otherwise the size
stays unset. Two cases are still not covered. First, this run's own dump
is missing from the listing while the operator Backup of a neighbour was
deleted or has not finished, then the neighbour's dump can be taken.
Second, backup pod clock and object store clock are both off the same
way by more than the minute of slack, which moves a finished neighbour's
dump into this run's window, and nothing the clocks record tells them
apart. A run whose Job failed has no dump of its own and reaches the
probe only because the driver takes `Complete=True` for success,
https://github.com/cozystack/cozystack/pull/5207 fixes that.

Of the open threads on the original PR, the cache reads, the dump file
name layout and the default AWS endpoint test were already handled by
its later commits, the clock skew one is the last commit here and the
failed Job case is https://github.com/cozystack/cozystack/pull/5207.

`minio-go` is new to this tree, this PR adds it as a direct dependency
and `internal/backupcontroller/artifact_size.go` is its only importer.
The open Bucket strategy PR
https://github.com/cozystack/cozystack/pull/4235 picks the same client
at the same version.

Each clock skew case, and a run whose own dump is gone while the
neighbour's operator Backup still exists and has finished, have a unit
test that fails on the old matcher, and every new guard has a mutant
that turns a test red. I also ran the matcher against a real bucket
after four MariaDB backups in a row. Runs that finished more than a
minute apart mapped to the dump their operator Job logged, the two back
to back came back unset instead of taking each other's dump, and with a
run's own dump hidden from the listing the matcher refused the
neighbour's dump that the old one returned.

### Screenshots

N/A, no UI changes.

### Downstream repositories

The only API change is the description of the existing
`Backup.status.artifact.sizeBytes` field, no new field, value, default
or enum, no package, values or tooling change, and neither
cozystack/website nor cozystack/terraform-provider-cozystack references
`sizeBytes`.

- [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(backups): MariaDB and CNPG (Postgres) backups stored in S3 now report `status.artifact.sizeBytes`: the size of the dump mariadb-operator uploaded, or the total size of the barman base backup (WAL archive excluded). A MariaDB backup takes the size of the one dump that falls inside its run on both the backup pod's and the object store's clock and inside no other finished run, and the size is left unset rather than set to 0 whenever it cannot be measured or attributed. Velero-based backups intentionally keep it unset.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Backup artifacts now report the stored size attributable to an
individual MariaDB or CNPG backup when it can be determined. Shared or
ambiguous storage may leave the size unset.
* **Bug Fixes**
* Improved handling of backup artifact creation and lookup errors,
including retrying when an existing backup cannot be read and failing
jobs when artifact creation is rejected.
* **Documentation**
* Clarified that an unset size means unknown; zero does not indicate an
unknown size.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-06 11:54:28 +05:00
Kirill Ilin 7f1227e3bd feat(installer): expose platformSourceCertSecret
Installing from a registry behind a private CA needs the operator's new
--platform-source-cert-secret flag; without a chart value it can only be
reached by editing the Deployment, which the next helm upgrade reverts.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-05 20:19:35 +05:00
Kirill Ilin c15a0d01b8 feat(operator): let the platform source trust a private registry CA
The operator generates the cozystack-platform OCIRepository from its own
arguments and rewrites it on every start, and cozystack-packages copies
that spec. A registry behind a private CA therefore had no durable way
to be trusted: a certSecretRef patched onto the live object is dropped
on the next operator restart, after which source-controller fails TLS
and platform updates stop arriving.

--platform-source-cert-secret sets spec.certSecretRef from the operator
itself, so the CA (or a client certificate) survives the rewrite. Git
sources are refused instead of ignored, because GitRepository has no
certSecretRef and Flux reads a Git CA from the source secret instead.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-11-05 20:19:29 +05:00
myasnikovdaniil e5bb3cda07 fix(kubernetes): move the OIDC users out of the bootstrap Job's script (#5213)
## What this PR does

Found while checking for other places with the failure from
https://github.com/cozystack/cozystack/issues/4968: data that grows with
configuration is passed to a process as one argument, and Linux allows
128 KiB for that. This one is in the `kubernetes` app and there is no
issue about it yet.

The OIDC bootstrap Job wrote a ClusterRoleBinding for every `oidc.users`
entry into its `sh -c` script, 526 bytes per user. I rendered current
main with `helm template` and users like `user123@example.org`: 238
users give a 130,889 bytes script that starts, 239 give 131,415 bytes
and `exec` fails with E2BIG. Then the container can't start, the
post-install and post-upgrade hook fails after its retries and the
HelmRelease of the tenant cluster stays unready. Schema has no limit for
`oidc.users` and each person needs own entry, so a cluster shared by 240
people gets there.

Now the bindings and the list of their names are rendered by the new
`oidc-rbac-bindings.yaml` into a ConfigMap `<release>-oidc-bindings`
that the Job mounts, so the script is 6,380 bytes for any number of
users. The ConfigMap has its own 1 MiB limit, about 2,500 users at 417
bytes each. Names, labels and roles of the bindings stay the same. I
compared the render of old and new chart for eight emails (ones YAML
reads as a boolean or null, a number, a quote, an apostrophe, unicode
and two plain) and the bindings are identical, so existing ones are
updated in place and nothing is orphaned.

The prune step reads the same ConfigMap. If the list is not mounted the
Job stops before it deletes anything, because `grep` on a missing file
exits 2, `! grep` is true and every binding of the release would be
pruned. A release with no users applies nothing (`kubectl apply` fails
on an empty file) and prunes all stale bindings, same as before.

`hack/kubernetes-oidc-bindings.bats` renders 300 users, checks the
script is under the limit without any email in it and that every user
has one binding with the old name and role. Three more tests run the
rendered script against a fake `kubectl` and check what it applies and
deletes. The prune condition forced to true or to false passes all
helm-unittest cases, so this part is tested in bats. The bug is in
release-1.6 and release-1.7 as well.

### Downstream repositories

I walked the trigger map against the diff (one chart template, its tests
and a bats file): no package added or removed, no values, schema or
default changed, no output Secret or Service renamed.

- [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(kubernetes): the OIDC bootstrap Job no longer fails to start for a tenant cluster with about 240 or more oidc.users, the bindings now come from a ConfigMap the Job mounts.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Kubernetes OIDC configuration now supports per-user role bindings,
with administrator and viewer access assigned according to each user’s
role.
* OIDC role bindings are kept in sync with the configured user list,
including removal of obsolete bindings.
* Deployments with no OIDC users or with OIDC mode disabled continue to
avoid creating user bindings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-05 19:37:16 +05:00
myasnikovdaniil ab13f9f72c fix(cilium): pass the CiliumNode list to jq on stdin in the IPAM hook (#5212)
## What this PR does

Fixes https://github.com/cozystack/cozystack/issues/4968. The cilium
IPAM migration hook passed the whole `kubectl get ciliumnodes -o json`
to `jq` as one argument, and Linux allows 128 KiB for a single argument.
Cilium copies Node labels onto every CiliumNode (22 to 30 KiB per node
with Talos, NFD or GPU labels), so the list is over the limit within six
nodes. `jq` can't start (`Argument list too long`), the hook Job fails
before it moves any node, `cilium` HelmRelease loops on `UpgradeFailed`
and everything that depends on networking stays in `DependencyNotReady`.

The hook is rendered on every upgrade of cilium in `cluster-pool` mode,
so a cluster that already finished the migration hits the same on its
next values change. The failing step only logs the pods that still hold
an old Ingress IP, but it runs before the node loop, so on a cluster
that is not migrated yet it blocks the migration itself.

Now the list goes to `jq` on stdin together with the IP objects, same as
the other `jq` calls in the script get their input.

The new test in `hack/cilium-ipam-migration.bats` makes six CiliumNodes
over the limit (one 24 KiB annotation each instead of labels, and it
checks the list is over 128 KiB even printed compact). It fails on the
old script with status 126 and passes now. It also checks that each pod
is named with the node whose Ingress IP it holds and that a pod on an
unrelated address is not named, because a join that always names the
first node, or reports every IP object, passed the old tests.

I did not take two suggestions from the issue. Skipping the hook when
every CiliumNode is in the pool changes what one existing test pins (the
hook adds the pool to `excludeIps` in that case) and doesn't help
clusters that are not migrated yet. A value to disable the hook leaves
the default broken. `cozystack-operator` rewriting `disableHooks` and
helm-controller rolling the DaemonSet on every retry are other
components. The bug is in release-1.6 and release-1.7 as well.

### Downstream repositories

I walked the trigger map against the diff (one hook script and its bats
test): no package, values, schema, default or output name changed.

- [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(cilium): the IPAM migration hook no longer fails with "Argument list too long" on clusters with more than a few nodes, which blocked the cilium upgrade and every package that depends on networking.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved IP address migration handling for environments with large
node lists, avoiding failures caused by operating system argument-size
limits.
* Migration continues to identify affected pods by node and distinguish
unrelated pods.
* **Tests**
* Added coverage for migrations involving large node lists, multiple IP
objects, and unrelated pods.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-05 19:37:05 +05:00
Myasnikov Daniil d7ae34dfc3 fix(backups): fail a ClickHouse BackupJob whose Backup name is taken
A Backup outlives the BackupJob that made it, so a BackupJob created
again under a name already used meets the earlier run's Backup when its
own Job completes. That happens by hand, and from a Plan reconciled
again inside its starting deadline, since a Plan names each BackupJob
after its schedule slot. The Altinity driver took the earlier Backup as
its result: the BackupJob turned Succeeded with a BackupRef recording
the earlier run's archive, the archive this run wrote was recorded on
nothing, and restoring what the BackupJob reported brought back the
older data.

An existing Backup is now taken as this run's only when it records this
run's archive, which is what an earlier reconcile of the same run
leaves when its status write is lost. Any other fails the BackupJob,
naming both archives, and leaves the earlier Backup untouched.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-05 07:56:44 +00:00
Myasnikov Daniil d5559bb934 test(backups): remove the Altinity strategy tests' scratch directories
Every test in hack/altinity-backup-strategy.bats made a directory with
mktemp -d and never removed it, so each passing run of the file left
nine directories in TMPDIR, and they pile up on any machine that keeps
it between runs.

Each test now removes its directory as its last statement. A failed
assertion aborts the body before that line, so a failing test keeps
its stub log and rendered script for inspection.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-05 07:56:39 +00:00
Aleksei Sviridkin 3d317c5f67 feat(api): expose HelmRelease failure state on Application status (#5171)
## What this PR does

Tenants usually read Applications but not the HelmReleases behind them,
and the Application status only carried the `Ready` and `Released`
conditions. A client could see that a release was failing, but not how
many times it had failed or whether helm-controller had stopped retrying
it.

This PR copies the `Stalled` condition onto the Application and adds
three optional status fields taken from the HelmRelease:
`installFailures`, `upgradeFailures` and `lastAttemptedReleaseAction`.
All of them are additive and `omitempty`, so existing clients are
unaffected.

What the new fields do and do not tell a client:

- With the `RetryOnFailure` policy the platform generates today, a
failed release is retried rather than stalled, so the counters are the
signal. That does not hold for every state: a release missing from Helm
storage while an install failure count is retained still stalls (the
case in #3554), a release on a remediation policy stalls once its
retries run out, and terminal errors such as an invalid CEL expression
or denied access stall too.
- `Stalled` is copied only when it was observed for the HelmRelease's
current generation. The Application carries no generation, so after a
spec update a client would otherwise read the previous spec's failure as
terminal for the new values; until helm-controller reconciles the new
spec, the Application shows no `Stalled`.
- The counters only count attempts that recorded a Helm release. A
failure before Helm records one, such as a chart that fails to render
(for example a `fail` guard rejecting the values), shows only as
`Ready=False` and leaves them at zero; a failure to apply the rendered
resources is counted.
- helm-controller resets the counters when the desired state changes
(generation, chart version, values), and on success only under
`RetryOnFailure`, so a `Ready` release can still carry non-zero
counters. The field descriptions say so.

`TenantModule` is left unchanged on purpose: it still copies only
`Ready` and `Released`.

Related: #5109, #4155

Tests cover the copied condition and counters, a `Stalled` left over
from an older generation after a spec update, the counters kept on a
`Ready` release, and that `Reconciling` is still not copied. The OpenAPI
schema and apply configurations are regenerated, and every per-kind
status schema picks up the new fields.

### 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 Terraform provider reads only the `Ready` condition by type, so the
extra condition and fields do not change its behaviour. The website only
shows an example Application output, not a description of the status
contract.

### Release note

```release-note
feat(api): Applications now expose the HelmRelease `Stalled` condition and the `installFailures`, `upgradeFailures` and `lastAttemptedReleaseAction` status fields, so tenants without access to HelmReleases can see how a release is failing. Tools that compute health with kstatus, such as Flux Kustomizations waiting on Applications, now report an Application whose release has stalled as failed.
```


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

## Summary by CodeRabbit

* **New Features**
* Application status now displays install and upgrade failure counts,
along with the most recently attempted release action. Failure counts
reflect attempts recorded as releases and reset when the desired state
changes.
* Current stalled release conditions are now reflected in application
status, helping clarify when a release is blocked.
* Failure details remain available when a release is ready, while
reconciling conditions are not shown.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-04 19:48:48 +03:00
Alexey Artamonov cd91138f33 fix(backups): re-render the default backup objects when the S3 endpoint changes
The S3 endpoint the platform backups use is copied twice and neither
copy is refreshed. The COSI sidecar writes BucketInfo once per
BucketAccess, so the credentials Secret rendered from it keeps its
endpoint, and the Velero BSL and the default Strategy CRs keep whatever
endpoint the last render of backupstrategy-controller saw. The default
objects gate only acted on objects that were missing, so after a
publishing.host change every backup kept going to the old host with
nothing reporting it.

The gate now also forces a render when a copy is stale: the bucket's
<bucket>-system release when the credentials Secret's endpoint differs
from the COSI Secret it is rendered from, and backupstrategy-controller
when the endpoint an object was rendered with differs from the
credentials Secret's. That endpoint sits at a different path in every
kind, in some only as a bare host, so the chart stamps the rendered
value on each object as backups.cozystack.io/endpoint and the gate
compares that. Objects rendered before the annotation existed count as
stale until one forced render has gone through, which stamps it.

The COSI Secret itself still holds the endpoint the BucketAccess was
granted with; it follows a host change only once the BucketAccess is
re-granted.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-04 19:13:55 +03:00
Aleksei Sviridkin c1ba05574f feat(monitoring-agents): keep application identity in container log streams (#5217)
## What this PR does

Makes Fluent Bit keep the pod lineage labels
`apps.cozystack.io/application.{group,kind,name}` on container log
lines, as the stream fields
`apps_cozystack_io_application_{group,kind,name}`, for both the default
container-log output and the per-tenant routed output.

Why: the per-application logs scope in the design proposal [Tenant
application logs](https://github.com/cozystack/community/pull/95)
(section 6) filters by these stream fields. Today the kubernetes filter
fetches pod labels, but they are dropped together with the rest of the
`kubernetes_*` keys before output.

What changes in the pipeline:

- A second `nest` lift of the pod labels, then a rename of the three
lineage keys to their final names before the existing `Remove_wildcard
kubernetes_` runs. All other labels are still removed. Every lifted key
is prefixed, so no other label can collide into the identity names.
- The three keys are added to `_stream_fields` of both container-log
outputs. Events and audit pipelines are unchanged.
- A parsed JSON log line is kept under `log_processed` (already on
`main`), so a line's content cannot create the identity keys.
- Guest Kubernetes clusters are not affected: their monitoring-agents
release replaces the filters and outputs wholesale. A regression test in
`packages/apps/kubernetes` pins that guest streams carry no identity
fields.

Effect on existing streams: VictoriaLogs treats a missing stream field
like an empty one, and the rename writes nothing when the label is
absent. Pods without lineage labels keep exactly the same stream. Pods
with lineage labels get a new stream once after the rollout; their
earlier lines stay reachable by namespace.

#### How it was tested

- helm-unittest for `monitoring-agents` (lift order, rename before
`Remove_wildcard`, stream fields on both outputs, events and audit
untouched) and for `kubernetes` (guest streams unchanged). The
events/audit guard was mutation-checked.
- Verified on a test cluster: real pod log lines arrive, Redis pods'
lines carry the three `apps_cozystack_io_application_*` stream fields,
and Fluent Bit logs show no errors.

#### Known limitation

Guest clusters write to the parent log store without authentication, so
a guest administrator can forge these stream fields as well as the
namespace field, within the store their tenant shares. This is the
write-path trust risk already documented in the proposal.

### Screenshots

No UI changes.

### Downstream repositories

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

Downstream: no trigger matched. Candidate: none affected, because the
diff changes `packages/system/monitoring-agents/values.yaml` (a system
package with no schema) and adds tests; the file under
`packages/apps/kubernetes` is a test only, so the app's `values.yaml`
and `values.schema.json` are unchanged.

### Release note

```release-note
feat(monitoring-agents): container log lines of pods that belong to a Cozystack application now carry `apps_cozystack_io_application_{group,kind,name}` stream fields in VictoriaLogs. Streams of pods without application labels are unchanged; streams of application pods start a new stream once after the upgrade.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Container-log streams now include application group, kind, and name
metadata for root and tenant stores.
* Guest monitoring streams include pod name, container name, and
namespace fields.
* **Bug Fixes**
* Application metadata is added to container-log streams without
changing the existing stream fields for event and audit outputs.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-04 18:27:20 +03:00
Aleksei Sviridkin e2283b3758 feat(monitoring-agents): export PVC labels from kube-state-metrics (#5215)
## What this PR does

Adds `persistentvolumeclaims=[*]` to the kube-state-metrics
`--metric-labels-allowlist`, next to the existing `pods=[*]` and
`deployments=[*]`. kube-state-metrics then publishes
`kube_persistentvolumeclaim_labels`, one series per PVC, with the PVC's
labels as `label_*`.

Why: the per-application metrics scope in the design proposal [Tenant
resource-consumption
metrics](https://github.com/cozystack/community/pull/94) (Phase 0) finds
an application's volumes through the `apps.cozystack.io/application.*`
lineage labels on its PVCs. Without this series, volume usage
(`kubelet_volume_stats_*`, which carries `persistentvolumeclaim` but not
`pod`) cannot be attributed to an application.

`[*]` rather than an explicit key list: kube-state-metrics has no prefix
matching, so the three lineage keys would have to be enumerated, and the
series count is the same either way (one per PVC). `[*]` stays
consistent with the existing pods and deployments entries and does not
silently miss a lineage key added later. PVCs are far fewer than pods,
which already export all labels.

#### How it was tested

- helm-unittest for `monitoring-agents`: a new suite asserts the exact
flag in the rendered kube-state-metrics Deployment.
- Verified on a test cluster: `kube_persistentvolumeclaim_labels` is
present and carries `label_apps_cozystack_io_application_name`.
- Not verified: the memory impact on kube-state-metrics.

#### Known limitation

PVCs created from a StatefulSet's `volumeClaimTemplates` usually have no
owner reference and no Flux label, so the lineage webhook gives them no
lineage labels and this series cannot attribute them to an application.
Fixing that belongs to the lineage webhook.

### Screenshots

No UI changes.

### Downstream repositories

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

Downstream: no trigger matched. Candidate: none affected, because the
diff touches only `packages/system/monitoring-agents/values.yaml` and a
test in that system package; it adds a series but renames no telemetry
metric, platform value, app field or CRD.

### Release note

```release-note
feat(monitoring-agents): kube-state-metrics now exports PVC labels as `kube_persistentvolumeclaim_labels`, one series per PVC, so volume usage can be attributed to the application that owns it.
```


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

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Kubernetes monitoring metrics now include labels from pods,
deployments, and persistent volume claims. This expands the resource
metadata available alongside metrics, including lineage labels used by
the tenant metrics API. Metrics for these resource labels are emitted
only for resources covered by the configured label allowlist.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-04 18:22:09 +03:00
Myasnikov Daniil 35a4f7c5d9 docs(release): split the release document into one page per topic
docs/release.md had grown into one page that mixed the release model,
the CI pipeline, changelogs, signatures, nightlies and the fires seen
at release time. A reader looking for one of them read all of it, and
an edit to one part reflowed the rest. It now indexes one page per
topic under docs/release/.

The pages already describe how fixes reach a release line through the
backport batch, which the following commits add. Workflows, scripts
and tests that named docs/release.md now point at the page that holds
the text they mean.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-04 15:03:06 +00:00
Myasnikov Daniil 59f0013b1f fix(backups): attribute a MariaDB dump only when no other run can own it
Every backup of a MariaDB application writes its dump under one shared
prefix, so the probe took the dump whose file-name timestamp (the
backup pod's clock) and LastModified (the object store's clock) both
fall in the operator Backup's lifetime, with a minute of slack for
skew. That keeps a neighbour's dump out only while this run's own dump
is in the window. With the pod clock more than the slack behind, the
own dump is named before the window opens, and a previous run that
finished within the minute before this one was created becomes the
single match: a 123-byte dump was reported as 666 bytes. A pod clock
ahead or an object store clock behind does the same with the next run,
and an own dump that is gone by the time it is looked up, as it can be
for a restore long after the backup, leaves a neighbour as the single
match with no skew at all.

A dump is now attributed only when ownership is proven:

- it is the one dump that either clock places in the window. A dump
  that one clock places there and the other does not may be this run's
  own with a skewed clock, so it leaves any other match unproven, the
  way a second dump in the window already did, and is not taken itself;
- both of its clocks place it in the window;
- neither clock places it in the window of another operator Backup
  that writes under the same bucket and prefix and has completed. This
  rejects a neighbour's dump when this run's own is missing, which the
  clocks alone cannot tell.

Operator Backups still running are not counted as neighbours: their
window has no end, and one stuck short of Complete (a Job whose pod
never starts stays JobRunning for good) would otherwise keep every
later run of the application from being sized. A dump such a run
writes inside this run's window still competes with this run's own.

Narrower slack does not prove ownership: a neighbour that finished
within the narrower slack still matches, and a smaller skew pushes the
own dump out. The Job's start and completion times are stamped by the
control plane, like the operator Backup's own, so they narrow the
window by seconds without removing the dependence on the two clocks a
dump carries. What is left: a neighbour's dump is taken only when this
run's own is missing from the listing or misplaced on both clocks and
the neighbour's operator Backup no longer exists or has not completed.
Every other unproven case leaves the size unset, as two runs that
overlap or follow each other within the slack already did.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-04 13:24:09 +00:00
Myasnikov Daniil 560c44f71e fix(backups): read a MariaDB dump from where its operator Backup wrote it
The size probe took the bucket, endpoint, prefix, credentials and TLS
settings from the strategy as it renders on the reconcile that creates
the Backup, which is not necessarily what it rendered when the operator
Backup was created: an existing operator Backup is reused untouched,
and a backup can run for up to the driver's 30-minute deadline. An
admin who edits the strategy in between sends the probe to the new
location, where it measures the wrong prefix or nothing at all.

The operator Backup's own spec.storage.s3 is where the dump went, so
the probe reads that. It is also the only source a restore can use,
since it targets that operator Backup long after the strategy may have
changed. An operator Backup that does not store to S3 is refused before
anything is listed.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-04 13:18:39 +00:00
Myasnikov Daniil 21f51a2e74 refactor(backups): return the matched MariaDB dump, not only its size
A MariaDB restore has to name the dump it restores from: without a
target time the operator restores the newest dump under the prefix,
which is not necessarily the one of the Backup being restored. The size
probe already finds that dump, but it returns only its size, folds a
missing, an ambiguous and an unreadable listing into the same 0, and
lives on BackupJobReconciler, which the restore reconciler is not.

Split the probe in two: findMariaDBDump reads the bucket and returns the
matched object (key, file name, the timestamp in its name, size and
LastModified) or an error that tells "not found" from "ambiguous"; the
size path keeps its timeout and reads only the size. The Secret lookup
and the S3 client move onto an objectStore value that any reconciler
can build from its own client, and BackupJobReconciler keeps one-line
wrappers so the CNPG probe is unchanged. sizeBytes is reported exactly
as before.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-04 13:16:15 +00:00
Myasnikov Daniil c140354977 test(backups): build operator Backups with the operator's success shape
mariadb-operator marks a Backup Complete=True for a failed Job as well
as a successful one; only the reason tells them apart (JobComplete with
message Success, or JobFailed with message Failed). The fixtures that
stand for a finished backup set Complete=True and nothing else, so they
describe a state the operator never produces and would read as a
failure to a driver that checks the reason. Give them the success shape
the operator writes.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-04 13:13:54 +00:00
Andrey Kolkov 92fa979690 fix(backups): trust only the endpoint CA where the backup writer does
With an endpoint CA the probe trusted the system roots plus that CA.
barman-cloud gets endpointCA as AWS_CA_BUNDLE, which replaces the
default trust store, so a CNPG probe could accept an endpoint the backup
itself would have refused. Let the connection say whether the CA adds to
the system roots or replaces them, and replace them for CNPG.
mariadb-operator appends its CA to the system pool, so MariaDB keeps
that.

Pin in reconcile tests that a Create the apiserver rejects still fails
the BackupJob, so only the read-back of an existing Backup is retried.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-04 13:04:23 +00:00
Andrey Kolkov 080df1b2f2 fix(backups): surface a Backup that cannot be read back and skip its probe
A Backup that exists but cannot be read back is retried through the
controller's backoff, which left the BackupJob Running with nothing on
it saying why. Set Ready=False with reason BackupNotReadBack before
handing the error back.

When the check for an existing Backup itself fails, the Backup may well
exist, so a size probe would only be discarded, and a read that keeps
failing reran it, up to the probe timeout on the single BackupJob
worker, on every retry. Skip the probe in that case and create the
Backup without a size.

Drive both behaviours through the MariaDB and CNPG reconciles in tests.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-04 13:04:22 +00:00
Andrey Kolkov 523d74a503 fix(backups): retry instead of failing when an existing Backup cannot be read
When Create collides with a Backup an earlier reconcile already made and
the read that follows fails, the drivers failed the BackupJob, although
the backup and its Backup both exist. Return a distinct error instead,
which the drivers hand back to the controller so the reconcile is
retried with backoff and then finds the Backup.

minio.New already refuses an endpoint with a path prefix and accepts a
trailing slash, so drop the duplicate check in the shared client; the
CNPG probe keeps its own check because it parses the URL itself and
would otherwise drop the path before minio sees it.

Say in the sizeBytes API doc what the value covers, such as the base
backup without the shared WAL archive for CNPG, and that strategies
which cannot attribute stored bytes to one backup leave it unset.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-04 13:04:22 +00:00
Andrey Kolkov 2b21763577 fix(backups): do not fail a backup on a transient retry-check read
The check for a Backup an earlier reconcile already created returned
its read error, and the drivers turn any error from that path into a
terminally Failed BackupJob. An apiserver timeout right after the
operator backup completed therefore lost a backup whose data was safely
in S3. The check only saves a probe, and the Create that follows already
handles an existing Backup, so log the error and go on to Create.

A trailing slash on the S3 endpoint is not a path prefix; the operators
accept it, so do not refuse it.

Pin the uncached AlreadyExists refetch, the upper slack of the MariaDB
window and the probe timeout with tests.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-04 13:04:22 +00:00
Andrey Kolkov a3d854739d fix(backups): attribute MariaDB dumps on two clocks and read retries uncached
The MariaDB probe identified this run's dump by the timestamp in its
file name, which comes from the backup pod's clock. With that clock
skewed past the slack, this run's dump falls outside the window while a
neighbouring run's dump can be the single match left, and its size is
reported as this backup's. A dump now has to fall inside the window by
both its name and its LastModified, which the object store stamps, so a
single skewed clock leaves the size unset instead.

The name parser also accepts the layout later mariadb-operator releases
write for compressed dumps, backup.<ts>.sql.<ext>, and checks the
compression token in either layout, so an operator upgrade does not
silently stop the measurement.

The retry check and the AlreadyExists refetch read the Backup through
the cache. A retry usually follows a Create made moments earlier; a
cache that does not show it yet turned the refetch into NotFound and
failed a backup that had succeeded. Read it from the API server instead.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-04 13:04:22 +00:00
Andrey Kolkov 3b86f57803 fix(backups): refuse to probe an S3 endpoint behind a path prefix
An endpoint such as https://s3.example.org/prefix cannot be addressed
by minio-go, which has no path-prefix option, and the prefix cannot be
added below the client either, because the SigV4 signature covers the
request path. The CNPG probe was worse than failing: it kept only the
host of endpointURL and listed the bucket at a location barman never
writes to.

Reject such endpoints with an explicit error, in the CNPG probe and in
the shared client, so the size is left unset instead of measured
somewhere else.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-04 13:04:22 +00:00
Andrey Kolkov 038f35376a fix(backups): do not report a partial listing or re-probe on retry
minio-go ends a listing whose context is cancelled between pages by
closing the channel without an error item. When the probe timeout
expired between pages of a large CNPG base backup, the pages already
read were summed and reported as the measured size. Check the context
once the listing channel closes and leave the size unset instead.

The probe ran before the Backup was created, so a reconcile that retried
after the Backup already existed probed the bucket again, held the
single BackupJob worker for up to the timeout, and then discarded the
result in favour of the existing object. Return an existing Backup
before probing.

Log the ambiguous MariaDB case (several dumps in the window) at info like
the other reasons the size stays unset, and cover the session-token
credential path with a test.

Assisted-by: LLM
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-11-04 13:04:22 +00:00