Commit Graph
7774 Commits
Author SHA1 Message Date
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
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
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
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
Alexey Artamonov 6e36385e00 feat(api): expose HelmRelease failure state on Application status
Tenants usually read Applications but not the HelmReleases behind them,
and only Ready and Released were copied over. A client could therefore
see that a release was failing but not how often it had failed, nor
whether helm-controller had stopped retrying it.

Copy the Stalled condition, which helm-controller sets once it stops
reconciling a release (remediation retries exhausted, rollback target
missing, invalid CEL, access denied), and surface the install and
upgrade failure counters with the last attempted release action.

Stalled is dropped when it was observed for an older generation than
the HelmRelease's current one: 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.

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 storage
while an install failure count is retained still stalls, and a release
on a remediation policy stalls once its retries run out.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-11-04 15:50:24 +03:00
Myasnikov Daniil 7b7444feaf fix(kubernetes): move the OIDC users out of the bootstrap Job's script
The bootstrap Job wrote a ClusterRoleBinding per oidc.users entry into
its script, and the script reaches the container as one `sh -c`
argument. Linux caps a single argument at 128 KiB, which about 240
users cross at roughly 530 bytes each. The container then cannot start,
the post-install and post-upgrade hook fails, and the tenant cluster's
HelmRelease stays unready. The values schema puts no bound on
oidc.users, so a shared cluster reaches that size with ordinary
per-person entries.

The bindings and the list of their names now reach the Job through a
ConfigMap it mounts, so the script is the same size for any list. Names,
labels and roles of the bindings are unchanged, so the bindings already
in a tenant cluster are updated in place and none is orphaned.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-04 07:06:31 +00:00
Myasnikov Daniil a2d0e4cb50 fix(cilium): pass the CiliumNode list to jq on stdin in the IPAM hook
The hook fed the whole `kubectl get ciliumnodes -o json` to jq as one
argument. Cilium copies every Node label onto its CiliumNode, 22 to 30
KiB per node on Talos or with NFD and GPU labels, so the list passes the
128 KiB Linux allows a single argument within six nodes. jq then fails
to start with "Argument list too long", the hook Job fails before it
moves any node, and the cilium HelmRelease loops on UpgradeFailed with
the HelmReleases behind it stuck in DependencyNotReady.

The hook renders on every upgrade of cilium in cluster-pool mode, so a
cluster that already finished the migration hits the same failure on its
next values change.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-04 06:57:03 +00:00
ZverGuy 95dc591198 test(kubernetes): keep application identity out of guest log streams
Guest pods are tenant-controlled, so their lineage labels are not a
trustworthy application identity, and the platform must not promote
them to the stream fields the parent scopes application logs by. The
guest release replaces the monitoring-agents filters and outputs
wholesale, which is what keeps the parent's label lift out of guests;
pin that so a later change to the guest overrides cannot bring it in.

This only covers what the platform ships. A guest admin can still edit
the guest's fluent-bit or write to the parent's vlinsert directly; that
needs a write-path control, which the logs proposal leaves open.

Assisted-by: LLM
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
2026-11-04 04:18:09 +07:00
ZverGuy e5f1714836 feat(monitoring): keep application identity on container log lines
A reader that scopes logs to one application needs the owning
application on every line, and the lineage webhook already puts it on
the pod as apps.cozystack.io/application.{group,kind,name}. Fluent Bit
fetched those labels and then dropped them with the rest of the
kubernetes map, so the store could only be scoped by namespace.

Keep the three labels as apps_cozystack_io_application_{group,kind,name},
the names the metrics side uses, and make them stream fields on every
container-log output so the scope is an indexed stream filter. A pod
without the labels produces no such fields, and VictoriaLogs builds the
stream from the fields present, so existing streams keep their
identity; a labelled pod starts a new stream once.

Assisted-by: LLM
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
2026-11-04 04:18:09 +07:00
ZverGuy 7de70ac2fc feat(monitoring-agents): export PVC labels from kube-state-metrics
The tenant metrics API scopes a query to one application by looking up
its pods and PVCs through their apps.cozystack.io/application.* lineage
labels. kube-state-metrics emits kube_persistentvolumeclaim_labels only
for resources on --metric-labels-allowlist, so without PVCs there the
PVC half of that lookup always comes back empty and volume series of
an application fall out of its scope.

All labels are allowed, as for pods and deployments: kube-state-metrics
emits one series per PVC whatever keys are listed, and PVCs are far
fewer than pods, which already export every label.

Assisted-by: LLM
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
2026-11-04 04:12:15 +07:00
myasnikovdaniil 4087218a02 docs: move the repo-only guides onto the website (#5034)
## What this PR does

Seven guides under `docs/` were operator facing and written alongside
the code, and left in the source tree where no user looks. They are on
the website now, so this deletes them here and repoints every reference.

```
docs/operations/backup-classes.md                  -> operations/services/backup-classes
docs/operations/resource-presets.md                -> guides/resource-management
docs/storage-immutability.md                       -> operations/storage-class-immutability
docs/operations/system-memory-limits.md            -> operations/configuration/system-memory-limits
docs/gateway-classes.md                            -> networking/gateway-classes
docs/observability/distributed-tracing.md          -> operations/services/monitoring/distributed-tracing
docs/operations/seaweedfs-capacity-for-backups.md  -> operations/services/object-storage/capacity-for-backups
```

`docs/release.md`, `docs/deprecating-applications.md` and the two
SeaweedFS runbooks in `docs/operations/` stay. A workflow, `AGENTS.md`,
a migration and a bats suite read them by path, which is the test from
#3777.

References point at `https://cozystack.io/docs/v1/...`. The site
rewrites that short prefix to the latest version in `layouts/404.html`,
so the links follow releases and pin none. Backup Classes, resource
management and `storageClass` immutability exist in the latest version
already. The other four pages are only in `next/` until 1.7.0 is
promoted, and their links start to resolve then. The generated mongodb
files come from `values.yaml` through `make generate` with
cozyvalues-gen v1.7.0, the version pinned in
`.github/workflows/pre-commit.yml`.

Tests: helm unit tests for mongodb (151), postgres (209), vm-disk (114)
and kubernetes-nodes (223), go tests for backupcontroller, presets and
the operator, rd-presets-check, the migration-50 bats suite.

### Downstream repositories

- [x] [cozystack/website](https://github.com/cozystack/website) -
follow-up: cozystack/website#763

Walked the rest of the trigger map against the diff. No provider
follow-up: the mongodb `values.schema.json` diff is one `description`
string, with no field added, removed or renamed, no enum, no default,
and no `kind`, `plural` or `release.prefix` change.
`packages/core/platform/values.yaml` changed in comments only and no
key, so the hand written platform-package table on the site is
unaffected.

Please merge after cozystack/website#763, the links in the code only
resolve once those pages deploy.

### Release note

```release-note
docs: seven guides move from the docs/ directory of this repository to cozystack.io (Backup Classes, resource presets, storageClass immutability, system component memory limits, GatewayClass selection, distributed tracing, SeaweedFS capacity for backups), and in-repo references point there.
```
2026-11-03 23:09:46 +05:00
Timofei Larkin e31f518df7 ci(api-review-gate): publish the gate as a commit status (#3440)
## What this PR does

Publishes the `API Review Gate` merge context as a commit status, so a
skipped or cancelled run cannot pass it or poison it, and gives fork PRs
the same gate through a privileged `workflow_run` workflow.

### Why

The merge context was a job name. A required job that skips counts as
passed, and check runs are not deduplicated per name, so one cancelled
run on a head keeps the context failed until the next push. Measured on
this repo: `41d1153c` has four check runs with the gate's job name (one
`cancelled`, three `success`). Commit statuses are deduplicated per
context and the newest wins, so a run with no verdict posts nothing and
the previous status stays.

### What changes

Same-repo PRs get status from `api-gate-report`. A `pending` opener
marks the head before the evaluation on approve, dismiss and
request-changes reviews, so an old `success` is not mergeable for the
minute the evaluation takes. `COMMENTED` reviews no longer count as a
reviewer's latest review, so an owner who approved and then commented
keeps the approval. Concurrency is split, so bot comment run cannot
cancel live evaluation.

Fork PRs: `api-review-gate-fork.yaml` runs from the default branch and
reaches the verdict itself, the fork's run is only a trigger. `resolve`
finds PR by head SHA in open PR list (review runs on a fork report base
repo as head repository and have no pull requests). `detect` fetches the
head as data and runs the detector in a job with `contents: read` only.
`conclude` applies the approval rule and posts. A sweep every 15 minutes
moves the status of fork PRs whose runs wait for a maintainer in
`action_required`, those never fire `workflow_run`. This is lllamnyp's
patch, applied whole, with these changes on top:

- two open fork PRs on one head fail closed in `resolve` and in the
sweep, a head carried by a same-repo PR stays with `api-gate-report`
- detector is built from default branch only, merge base is picked by PR
author and an old one would mean an old detector
- the fork `detect` step runs under `bash -e` and detector exit 2 ended
it before the code was read, so it has `set +e` like the same-repo job
- head is checked out with links as links, like the same-repo gate does,
because flattening them hid a CRD behind a file link from the detector
while Helm still ships it

### Limits

- A fork PR whose push run waits for a maintainer's approval has no
status until that run executes, same as `E2E Tests`.
- A fork PR on a head that a same-repo PR also carries shows the verdict
standing there, one commit status cannot hold a verdict per base.
- The sweep can publish a stale `success` if it reads an approval,
stalls, and the owner withdraws it meanwhile. The next sweep flips it
back, so the window is up to 15 minutes.
- Two copies of one first-party CRD and CRDs behind directory links are
gaps of the detector on main, same for same-repo PRs, they are in
https://github.com/cozystack/cozystack/issues/5170.

### Testing

`actionlint` is clean on both workflows. `zizmor` is clean too, but
`dangerous-triggers` and `template-injection` are disabled repo-wide in
`.github/zizmor.yml`, so that says little about a privileged workflow. I
ran the `github-script` bodies under node with a mocked API (every
conclusion, approval and shared head case) and the run steps under `bash
-e` with a stub detector exiting 0, 2 and 1. `make unit-tests` passes
with `LC_ALL=C` (a kafka bats test depends on the locale, nothing to do
with this PR).

Not exercised live: the fork path and the sweep run only from the
default branch, so their first run is after merge.

### Repo admin

Branch protection requires the old job name `Require API owner review
for sizeable API changes`. This PR renames that job, so after merge the
required context has to be changed to the `API Review Gate` status.

### Downstream repositories

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

### Release note

```release-note
NONE
```
2026-11-02 19:15:18 +04:00
Aleksei Sviridkin a2cc9a3016 fix(dashboard): link a tenant row to its detail page (#4004)
## What this PR does

Fixes #3822

Tenant list showed name as plain text and the only link in a row was
Edit button, so tenant detail page with its tabs and Delete was
reachable only by opening edit form and cancelling out of it. Name cell
is a link to that page now, it appears exactly where Edit appears and
points to the same CR. Plain click switches selected tenant to the row's
parent first, same as Edit does, because detail page takes namespace
from selected tenant. Ctrl, cmd and shift click don't run that switch,
so the tab you open a tenant from stays where it was.

First commit fixes what the link depends on. Console recovers a tenant's
parent from namespace name (`tenant-a-b` sits under `tenant-a`), but
root is the one level that naming skips: tenant `acme` ordered in
`tenant-root` owns `tenant-acme`, not `tenant-root-acme`. So
`realParentNamespace` returned undefined for every top level tenant,
which is most tenants on a normal cluster, and Edit button has been
missing there for as long as it exists. Ancestor labels carry the parent
that the name does not. Root level tenant named `root-x` resolves to CR
`root-x` now, not `x`.

### Notes

I first tried to make links carry their tenant in the url (`?tenant=`),
so a detail page opened in new tab resolves on its own. That turned into
questions about tenant context itself: what to do with a tenant in a
link the user can't see, what localStorage may hold, and TenantNamespace
LIST ignoring `labelSelector`. Detail route addresses a Tenant CR by
relative name and takes namespace from selected tenant, and making that
robust is a refactor of tenant context and routing, which should start
with study and planning, not with patches on patches. So this PR only
closes the UX gap (no way from tenant list into a tenant's page), going
deeper is out of scope.

Known limit that comes with it: link has no tenant in it, so opening it
in a new tab (ctrl/cmd/middle click, open in new tab, copied url)
resolves against the tenant last selected in any tab, because selection
lives in shared localStorage. For a row directly under that tenant it
opens the right page. For a row deeper in the tree the page says
`Application <name> not found.` and there is no Delete. If that tenant
also has a direct child tenant with the same relative name, new tab
opens that other tenant's page with working Edit and Delete, and delete
confirm says only `Delete Tenant "<name>"?`. Plain click in the same tab
is not affected. Detail page's own tabs and Edit link already behave
this way, it's how detail route resolves its namespace today and fixing
it properly is the refactor above.

A row links, and offers Edit, exactly when namespace holding its Tenant
CR is one the apiserver returns to this user. It returns a tenant
namespace only if the user is subject of a RoleBinding in it, so
sub-tenant admin that cant act in `tenant-root` sees no link there.

No row level Delete button. Issue allows either that or a route to
detail page, I took the route so confirm and mutation stay in one place.

Covered by tests, every source change here fails a test when reverted:

- top level tenant, nested one, root level tenant named `root-x`, forest
root without ancestor labels
- bridged row whose real parent is invisible gets neither link nor Edit
- plain click switches to the row's parent and opens detail page
- ctrl click leaves selection and location alone

### Screenshots

Tenant list under root on a dev cluster. Every row name is a link to
tenant page now (`pr4078` is hovered), top level tenants had neither
link nor Edit before. Host column redacted.

<!-- SCREENSHOT: drag in 01-tenant-list-links.png here -->
<img width="1440" height="900" alt="01-tenant-list-links"
src="https://github.com/user-attachments/assets/39cd723a-1ac5-4d3b-8c36-b70ddc626961"
/>


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

All changed files are under `packages/system/dashboard/images/console`,
console has no downstream copy (standalone UI repo is archived).

### 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(dashboard): tenant name in the tenant list links to its detail page now (where Delete is), and top level tenants get their Edit button back.
```



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

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

## Summary by CodeRabbit

* **Bug Fixes**
* Root-level tenant names are now displayed without incorrectly removing
prefixes, and tenants ordered directly under the root are correctly
recognized in the hierarchy.
* Tenant rows link to their detail pages when the parent tenant is
accessible. Same-tab navigation updates the active tenant, while opening
a link in another tab leaves the current selection unchanged.
  * Edit links and navigation now use consistent tenant routes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-02 16:33:51 +03:00
Myasnikov Daniil b5e3b37a9a ci(api-review-gate): build the fork detector from the default branch
The merge base is the commit the fork's author branched from, so a
branch cut from an old commit selected an old detector, and a fix to
the detector never reached the PRs an attacker controls. The workflow
itself comes from the default branch, so the detector now does too.

The head is also checked out with its links intact. Flattening them
hid a CRD behind a file link from the detector while Helm still
shipped it, and made the verdict differ from the one the same-repo
gate reaches.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-02 14:47:51 +05:00
Aleksei Sviridkin d32092ac9a feat(release): attach an SBOM to every release image (#4997)
## What this PR does

This PR attaches an SPDX SBOM to every first-party image of a release.
BuildKit adds it at build time as an attestation, so it sits in the
image's index and moves with the digest the release pins, through
promotion. `docker buildx imagetools inspect <image>@<digest> --format
'{{ json .SBOM }}'` prints it, one document per platform. Releases cut
from this change on have it. The Talos installer image (built by the
imager, pushed by skopeo) and the third-party images don't.

It is more than `SBOM: '1'` in `tags.yaml`, because three things broke
on the way:

- An image built with an attestation is an index of the image and an
attestation manifest. `image_arch` in `hack/stitch-multiarch.sh` read
that as not one amd64 and one arm64 image, so the stitch skipped every
such image and still exited 0, and `hack/verify-multiarch.sh` then
failed the release with "index without linux/arm64". `image_arch` and
the rerun check look through the index now, and the stitched index keeps
both attestations.
- The release `make build` ran on the default docker driver, which
cannot attach attestations. It runs on the docker-container builder with
the docker.io mirror now, like the arm64 leg. `use: false` stays, so a
plain `docker build` keeps the default driver.
- `--sbom=true` pulls `docker/buildkit-syft-scanner:stable-1`, a
floating tag. `SBOM=1` passes `--attest type=sbom,generator=` with the
scanner pinned by digest (1.12.0). The comment in `hack/common-envs.mk`
called the output CycloneDX, BuildKit writes SPDX, and the same word in
`docs/security/self-assessment.md` is fixed too.

Only the release lane changes. PR, main and nightly builds push to OCIR
and are not what users install, so the OCIR attestation probe the
tracker (A2 in #3278) listed is not on this path, release images go to
GHCR.

### How it was checked

- Bats 1.14.0: four new tests in `hack/stitch-multiarch_test.bats` (the
arm64 leg's workflow shape, a stitch of SBOM images, a rerun after one,
an index of two linux images that is skipped), two in
`hack/common-envs_test.bats` (the flags, and the Renovate manager held
to the `SBOM_SCANNER` line), and the matchbox workflow test rewritten
for the new builder. The stitch tests fail on `main`'s
`stitch-multiarch.sh`, and each of these mutations fails one: dropping
the `length == 1` check, counting the attestation as a platform, looking
for the amd64 index itself in the rerun, taking `SBOM`, `BUILDER` or the
mirror config out of `tags.yaml`, going back to `--sbom=true`, dropping
the scanner digest, turning `SBOM` on by default, reformatting the
`SBOM_SCANNER` line out of the Renovate manager's regexp.
- A rehearsal on a localhost registry (docker 29.8, buildx 0.37.1) with
the real scripts. Images built through the `BUILDX_ARGS` of
`common-envs.mk` for amd64 and arm64, one without SBOM beside them, then
`stitch-multiarch.sh` (2 stitched, 0 skipped), `verify-multiarch.sh` (0
failed), `imagetools inspect` reading SPDX 2.3 with 15 packages per
platform from the stitched index, and `skopeo copy --multi-arch all`
keeping the index digest. `containerimage.digest` in `--metadata-file`
equals the pushed index digest, so the digest the package Makefiles
stamp is the index. The same run with `main`'s stitch skips the SBOM
image, exits 0 and fails the gate.
- `make bats-unit-tests`: 2444 of 2445. The one failure, "all-topics
backup skips double-underscore internals and keeps single-underscore
user topics" in `hack/kafka-topic-backup-strategy.bats`, also fails on
unmodified `main` on the same machine. `make bats-posix-compat-tests`
passes, and the stitch test file passes under dash through
`cozytest.sh`. `hack/check-commit-trailers.sh` passes. zizmor and
actionlint aren't installed here, so the workflow change (two env keys
and one renamed builder step) is covered only by the tests above.
- Not checked: that GHCR takes the attestations, what retention does to
the child manifests, the fork lane's OCI export (it doesn't set `SBOM`),
and a real release build. The first alpha cut after the merge tests the
first and the last. Renovate itself did not run on the new manager: the
config passes the Renovate JSON schema, and the regexp takes the tag and
the digest off the live line, which a test holds.

### Decisions for the release owners

- Scope is `tags.yaml` only. SBOMs on main and nightly images would need
OCIR to take attestations (not probed) and would change what the nightly
mirror copies.
- The scanner is 1.12.0 with syft 1.51.0 inside, which is in the range
of GHSA-mw2c-m758-9v5q (memory exhaustion parsing binary inputs, fixed
in syft 1.52.0). It reads images our own builds produce, so I didn't
hold the change for it. A Renovate manager on the `SBOM_SCANNER` line
moves the version and the digest together when the scanner ships a newer
syft.
- The scanner adds a step per image and I didn't time it on the release
runners.
- A stitch rerun reads the amd64 index the tree pins, which stays in the
registry untagged after a stitch. If the registry drops untagged
manifests before the rerun, it fails at that read rather than repinning.
- After the first alpha with attestations I'll move A2b in #3278 and the
SBOM item in #2937 to done. The SBOM row in
`docs/security/self-assessment.md` still says SBOMs aren't published,
and stays until that alpha shows they are.

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

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

I walked the trigger map against the diff. It touches `tags.yaml`,
`hack/common-envs.mk`, the stitch and verify scripts and three docs. No
release asset is renamed, `hack/package.mk` and the commit convention
are untouched, and the flow a maintainer follows to cut and promote a
release is the same.

### Release note

```release-note
feat(release): every first-party container image of a release carries an SPDX SBOM as a BuildKit attestation in its index, readable with `docker buildx imagetools inspect <image>@<digest> --format '{{ json .SBOM }}'`. Releases cut from this change on have it, the Talos installer image and third-party images don't.
```


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

## Summary by CodeRabbit

- **New Features**
- Supported release image builds now include SPDX-format software bills
of materials (SBOMs) as attestations.
- Multi-architecture image stitching, retagging, and mirroring preserve
SBOM attestations.
- **Documentation**
- Updated image-reference guidance to explain how to handle attestation
entries in multi-architecture indexes.
- Clarified which images include SBOMs, how to inspect them, and that
SBOM generation remains disabled by default outside configured release
builds.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-01 22:35:38 +03:00
IvanHunters 94ada11eef fix(marketplace): pin a tapped repository to a digest instead of a mutable tag (#5082)
## What this PR does

A tapped (community-connected) repository followed a mutable OCI tag:
the Flux `OCIRepository` carried only `spec.ref.tag` with a 5-minute
interval, so whoever controlled the tag controlled what ran in the
management cluster within that interval, down to the HelmReleases of
already-running tenant application instances. Fixes #4979.

A tap is now pinned to a digest. `cozypkg tap` and the connect API
accept an explicit `oci://host/org/repo@sha256:...` reference; a tag
reference is resolved and pinned to its digest by the tap materializer
once the artifact for the current spec generation is ready, from the
digest Flux reports in `status.artifact.revision`, with the tag kept
informational. A tap connected before this change is pinned to its
current digest on the first reconcile after upgrade, and a re-connect to
a new tag waits for source-controller to re-resolve before pinning, so a
stale revision is never pinned. Moving the tag upstream no longer
changes anything in the cluster; an explicit re-connect moves the pin.

Connect also accepts an optional keyless cosign verification policy
(`--certificate-identity` / `--certificate-oidc-issuer` on the CLI,
`spec.verify` on the API), anchored to an exact identity so a
superstring cannot match, and preserves it, and the pull secret, across
repeat connects rather than silently dropping them. With those flags the
CLI verifies the signature at tap time and pins the verified digest
before pulling, so the content it applies is what a trusted identity
signed; it fails closed if the verified digest cannot be determined.

### Screenshots

Not applicable: no UI changes.

### Downstream repositories

I walked the trigger map against the diff. The change is confined to
`cozypkg`, the `Tap` aggregated API (a virtual, cluster-scoped resource
with no CRD), the tap materializer, and the community-index docs. It
does not touch `packages/apps`, `packages/core/platform/values.yaml`,
installer values, platform variants/components, release assets,
`ApplicationDefinition` semantics, the `hack/` make targets, or the
commit convention, so the provider, ansible, ccp and external-apps paths
are not reached.

One path is arguable and needs a human decision, so I have left every
box unticked rather than tick or dismiss it:

- `cozystack/website` — `cozypkg tap` gains flags and digest-pinning
behaviour. The marketplace tap flow is new and I am not sure it has a
reference page on the site yet, so whether a docs follow-up is warranted
(and where) is a maintainer call.

- [ ] 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(marketplace): a tapped repository is pinned to a digest instead of following a mutable tag, so moving the tag upstream no longer changes what runs in the cluster; connect accepts oci://...@sha256:... and an optional keyless cosign verify policy (anchored to an exact identity and preserved across re-connects), and with verify flags the CLI verifies the signature and pins the verified digest at tap time.
```


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

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

## Summary by CodeRabbit

- **New Features**
- OCI repositories can be connected using a SHA-256 digest, with or
without a tag. Tag-based sources are pinned to the resolved artifact
digest.
- Optional Cosign keyless verification is available during connection
using certificate identity and OIDC issuer details.
- Reconnecting preserves existing verification and pull-credential
settings when replacement values aren’t provided.
- **Bug Fixes**
- Reconnecting with a new tag clears previous operator-managed pins so
the requested tag can resolve to its current artifact.
- **Documentation**
- Clarified tag pinning, signature verification, and where tapped app
charts run.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-30 18:09:34 +03:00
IvanHunters ccc216652e fix(monitoring-agents)!: isolate merged log payload under log_processed (#5167)
## What this PR does

Fluent Bit's `kubernetes` filter runs with `Merge_Log On` but without
`Merge_Log_Key`, so a parsed JSON log line is merged into the record
root.
An application that logs JSON can thereby set top-level fields that
later
pipeline stages trust for routing and tenant isolation:
`kubernetes_namespace_name`
(rewrite_tag routing and the `_stream_fields` stream identity),
`kubernetes.labels`, and the `tenant` / `cluster` / `log_source`
markers.
A tenant workload can forge another tenant's values on its own log
lines.

Setting `Merge_Log_Key log_processed` nests the parsed payload under a
dedicated key, so application log content can no longer override the
top-level service fields.

The change is applied to both Fluent Bit configurations that carry this
filter: the platform agent
(`packages/system/monitoring-agents/values.yaml`)
and the guest-cluster agent (`packages/apps/kubernetes`).

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff,
file by file. The change touches only the Fluent Bit filter config
inside
`monitoring-agents`; it does not alter package layout, any
`values.schema.json`,
platform values keys, `ApplicationDefinition`, any CRD, `hack/`,
namespaces,
telemetry metric names, or the commit convention. None of the listed
couplings match.

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

### Release note

```release-note
fix(monitoring-agents)!: Fluent Bit now nests parsed JSON log content under a `log_processed` key instead of merging it into the record root. This prevents an application log line from forging the service fields used for log routing and tenant isolation. BREAKING: top-level fields previously extracted from JSON logs (for example `level`, `msg`, and any application-specific keys) now live under `log_processed.*`; external dashboards and LogsQL queries that read them from the record root must be updated.
```


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

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

## Summary by CodeRabbit

* **Improvements**
* Merged Kubernetes log content is now grouped under `log_processed` in
Fluent Bit output.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-30 17:14:45 +03:00
Myasnikov Daniil 7f55ff5d85 fix(dashboard): link a tenant row to its detail page
The tenant list rendered the name as plain text and put the row's only
link on an Edit button, so nothing in the list reached the detail page
that every other kind gets, tabs and a Delete action included. The only
way there was to open the edit form and cancel out of it.

The name cell becomes a link wherever Edit is offered, to the same CR.
The detail page reads the namespace from the tenant context, so a plain
click aligns the context to the row's parent first, the way Edit
already does. React runs that handler for a ctrl-, cmd- or shift-click
too, which the browser turns into a new tab or window, so it runs only
for the click that navigates its own tab; otherwise opening a tenant
beside the current one would move the tab left behind as well.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-30 16:38:53 +05:00
Myasnikov Daniil e44e41d307 fix(dashboard): resolve the Tenant CR of a tenant ordered in the root
A tenant's CR lives in its parent's namespace, and the console recovered
the parent from the namespace name: `tenant-a-b` sits under `tenant-a`.
The root is the one level that naming skips. A tenant `acme` ordered in
`tenant-root` owns `tenant-acme`, not `tenant-root-acme`, so the prefix
test never matched a top-level tenant and `realParentNamespace` returned
undefined for it. That is most tenants on a normal cluster, and the Edit
button, offered only where the CR's namespace resolves, was missing for
all of them.

The ancestor labels carry the parent the name does not, so the lookup
falls back to `tenant-root` when the chain names it. Deriving the CR name
keys on the parent being the root rather than on the child's name
happening to start with it, which keeps a root-level tenant called
`root-x` from resolving to the CR `x`, and the row label agrees with it
rather than showing `x`.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-30 16:23:10 +05:00
Myasnikov Daniil dfa2283a13 ci(api-review-gate): close a standing success on a shared fork head
A commit status belongs to the commit. When two open fork PRs carry the
same head, a success already standing on it may have been earned by one
of them on another base, or may rest on an approval that was withdrawn
on a PR whose run waits for a maintainer and so never reaches resolve.
The sweep skipped such a head and left that success in place, which let
a duplicate fork PR keep a withdrawn approval from taking effect.

The sweep now writes the same failure resolve writes, once per head,
when a success set by a workflow stands on a head that only fork PRs
carry. It still never creates a status, and a head that an open
same-repo PR carries stays with api-gate-report.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-30 16:09:00 +05:00
Myasnikov Daniil 0538e70591 ci(api-review-gate): let a sizeable fork change reach the approval check
api-gate exits 2 to report a sizeable change, and the runner runs a step
without an explicit shell as bash -e. The fork detect step switched
errexit off only after reading the exit code, so the first failing
command ended the step with 2 before sizeable=true was recorded. Every
fork PR with a sizeable API change then got "gate evaluation did not
complete", which the sweep deliberately ignores, so an owner's approval
could not turn it green by either path. The same-repo step already
switches errexit off before the call.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-30 16:09:00 +05:00
Myasnikov Daniil 878f569e61 ci(api-review-gate): leave a shared head to its same-repo publisher
A commit status belongs to the commit, so two open PRs that carry the
same head share one "API Review Gate" status. The fork workflow
attributed that status to a fork PR by head SHA alone. A fork PR opened
at a commit of this repository could therefore overwrite the verdict of
the same-repo PR that owns it: resolve wrote a failure after
api-gate-report had posted success, and the sweep applied the fork PR's
reviews to a status the other PR had earned. A fork can reference any
commit of the repository's network, so anyone able to open a PR could
hold another PR's gate red.

The fork workflow now leaves a head that an open same-repo PR carries to
api-gate-report, and the sweep skips any head carried by more than one
open PR. Two fork PRs at one head still fail closed in resolve, since
their bases, and so their verdicts, can differ.

The header of the same-repo workflow said skipped and cancelled runs
post nothing. That stopped being true once a review posts pending ahead
of its evaluation, so it now says they post no verdict.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-30 16:09:00 +05:00
Myasnikov Daniil 4c37ef3a61 ci(api-review-gate): let owner approval reach fork PRs
On a fork PR an API owner's approval could never turn the status green.
For a pull_request_review run on a fork PR, workflow_run.head_repository
names the base repository and pull_requests is empty, so the fork
publisher took every review-triggered run for a same-repo one and
skipped it, while the same-repo reporter skipped it for being a fork. A
sizeable fork PR stayed on the failure its push run had posted.

Fixing the guard alone would leave most forks stuck. The repository
makes every run from an external contributor wait for a maintainer's
approval, and a run in action_required never fires workflow_run, so
their review-triggered runs never reach the publisher. And a fork's run
executes the fork's own copy of the gate workflow, so the conclusion the
publisher relayed was whatever the fork made it.

The privileged workflow therefore reaches the verdict itself, from
inputs a fork cannot forge, and a scheduled sweep covers the runs that
never execute. The sweep only moves a status the full evaluation already
set after finding a sizeable change, so it cannot create a pass.

The same-repo gate now posts pending as soon as an actionable review
arrives: a dismissal can turn a green head red, and the old success
would stay mergeable for the minute the evaluation takes. Both paths
also ignore COMMENTED reviews when taking each reviewer's latest, since
an owner who approved and then commented had withdrawn their own
approval as far as the gate was concerned, which GitHub does not do.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-30 16:09:00 +05:00
Myasnikov Daniil 939496ad78 docs(e2e): record the gate-status pattern's second consumer
The "E2E Tests" commit-status pattern in §10 now has a second consumer
in api-review-gate.yaml, and the two GitHub behaviours that make it
load-bearing were not written down anywhere.

Check runs are not deduplicated per name across workflow runs — each run
gets its own check suite, so filter=latest returns the same set as
filter=all and the aggregate is worst-of, meaning a cancelled run
poisons a context until the next push — while commit statuses are
deduplicated per context with the newest winning. And the fork
read-only token restriction covers pull_request_review as well as
pull_request, so an approval on a fork PR cannot publish a status
either, which is why both gates need a privileged workflow_run half
rather than only handling the push path.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-30 16:09:00 +05:00
Myasnikov Daniil b771962eb8 ci(api-review-gate): publish the gate as a commit status
The gate's merge context was its job name, which inherits two GitHub
behaviours that together make it both unenforceable and self-poisoning.

A required *job* that skips counts as PASSED by branch protection, so
any path that skips the evaluating job hands out a free pass — the hole
that let fork PRs merge with e2e never run (#3257). And check runs are
not deduplicated per name: every workflow run gets its own check suite,
so `GET /commits/{sha}/check-runs?filter=latest` returns the same set as
`filter=all`, and the rollup is worst-of. Two runs on one head SHA
therefore leave two verdicts standing for one context name, and a
cancelled run poisons the context until the next push. Commit statuses
are deduplicated per context, newest wins.

Rename the evaluating job off the gate context and publish "API Review
Gate" as a commit status: `api-gate-report` for same-repo PRs, and a
privileged workflow_run for forks, whose GITHUB_TOKEN is read-only on
both the pull_request and the pull_request_review trigger and so can
never post a status itself. Skipped and cancelled runs post nothing, so
declining to post can only ever preserve a verdict some real run
reached — it can never invent one.

That invariant is what makes it safe to stop re-evaluating on the plain
`commented` reviews the review bots post: such a review cannot move a
verdict that counts only APPROVED reviews, and the run that skips now
leaves the previous status untouched instead of overwriting it.

Also split the concurrency group so a run that computes no verdict
cannot cancel one that does. A bot commenting during the ~30s
evaluation used to cancel it and then contribute nothing, leaving a
cancelled run as that SHA's only api-gate verdict (41d1153c, 4b1966c5).

For the gate to actually block a merge, main branch protection must
require the "API Review Gate" status context; until then it is
advisory, as the job name was.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-30 16:08:59 +05:00
IvanHunters a444ac4498 Merge branch 'main' into fix/tap-pin-digest
Reconcile the tap digest-pin change with #5071 (no-install-on-connect), now
merged into main. Keep this branch's resetTapPinState, the verify-before-pull
Trust text, and the index-README trust model, while adopting #5071's wording
that installing (not tapping) runs charts and dropping the stale
"re-registration" phrasing from the shared tap.go and rest.go comments.

Conflicts resolved in cmd/cozypkg/cmd/tap.go, pkg/registry/core/tap/rest.go,
and hack/packages-index/README.md.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-10-30 14:02:24 +03:00
IvanHunters e1e925a092 docs(agents): steer agents away from Helm lookup (#5168)
## What this PR does

Adds a rule to the Helm Charts conventions in `docs/agents/overview.md`
about `lookup`. A lookup reads cluster state at render time and nothing
re-renders the HelmRelease when that state changes, so a lookup that
copies another object's content into the output (a bucket name, an
endpoint, a CA, a StorageClass list) goes stale; several have failed
this way (51ad22670, 8daf49528, f6c737983). An agent reading the charts
finds over a hundred lookups and takes them as the convention, so the
rule says those of this kind are not precedent and need a maintainer's
authorization.

Lookups whose repeated render gives the same output and carry nothing
from another object are allowed: existence checks, keeping a generated
Secret's value, keeping an immutable field of the chart's own live
object, and guards that `fail` on an unsafe change. The rule asks that
the empty branch keep rendering, since offline `helm template` and
helm-unittest without a `kubernetesProvider` mock see only that branch,
and lists the replacements the tree already has: `_cluster`/`_namespace`
in the `cozystack-values` Secret, `valuesFrom` on a specific Secret,
`.Capabilities.APIVersions.Has`, a run-time check in a hook or Job, or a
controller that owns the object. `AGENTS.md` mentions the rule in its
routing line for `overview.md`, which every agent loads. No chart
changes.

### Screenshots

No UI change.

### Downstream repositories

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

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

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

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

Walked the trigger map against the diff: two agent-guidance files, no
package, chart, value, `hack/` or commit-convention change.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [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
docs(agents): agent guidance now says which Helm `lookup` calls are allowed in chart templates, that one copying another object's content into the render needs a maintainer's authorization, and what to use instead.
```


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

## Summary by CodeRabbit

* **Documentation**
* Updated Helm chart guidance to restrict copying values from other
Kubernetes objects into rendered output unless a maintainer authorizes
the lookup. Allowed cases include repeatable lookups, such as existence
checks, retaining generated Secret values or immutable fields, and
guards against unsafe changes.
* Clarified that offline renders should preserve the empty branch, while
the lookup branch should be tested with a Kubernetes provider mock.
Listed values, API version checks, runtime hooks or Jobs, and
controllers as alternatives for accessing cluster information.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-30 13:46:28 +03:00
Timofei Larkin 321baf5707 docs(agents): narrow lookup rule to copied state
The staleness problem is specific to a lookup that carries another
object's content into the rendered output; a lookup whose repeated
render is identical, such as a guard or keeping a generated Secret,
cannot go stale, and banning it pushed agents toward nothing better.
The testing caveat held only for runs without a kubernetesProvider
mock, and the alternatives now name the mechanisms the tree already
has, so an agent can reach for them instead of a new lookup.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-10-30 10:37:15 +01:00
Aleksei Sviridkin ba0b1ac6ef fix(e2e): validate the leak allowlist against every suite layout (#4972)
`lg_allowlist_validate` in `hack/e2e-chainsaw/_lib/leak-guard.sh`
accepted only `<suite>/chainsaw-test.y*ml`, while the selectors already
see step-file suites and a Test nested below the suite directory
(#4630). A suite in one of those layouts could not be put on the leak
allowlist at all: validation failed and the run stopped before the first
suite.

- `lg_suite_has_test` validates an allowlist entry with the rule
`hack/select-e2e.sh` enumerates suites with: `chainsaw-test.yaml`,
`chainsaw-test.yml`, or a numbered step file, anywhere in or below the
suite directory, with the selector's type test (a step-file name counts
unless it is a directory, a `chainsaw-test.y*ml` counts whatever its
type, so Chainsaw reports a directory by that name); a parked
`chainsaw-test.yaml.disabled` matches none. Both sides carry a comment
pointing at the other so they move together.
- `hack/e2e-chainsaw/README.md` describes the three layouts once, under
Layout, and lists `kubernetes-proxmox` as the parked suite.
`docs/agents/e2e-testing.md` points at it.
- Unit tests: every layout is accepted, including a `chainsaw-test.yaml`
that is a directory; a parked, empty, or not-a-step-file directory is
refused, including `1a-install.yaml` (passes the `find` glob, not the
step-file regex) and a directory named `01-install.yaml`.

Left as they are, with the reasons:
- `docs/agents/e2e-testing.md` calling `kubernetes-proxmox` the one
parked suite is correct on main now: `hack/e2e-chainsaw/backup/` was
removed in 9cc3a9e45.
- The top-level globs in `hack/e2e-chainsaw-run.sh` are wider than
Chainsaw's step-file pattern, but they only refuse a test at the top
level and fail closed.
- A file under `examples/backups/<other>/` that matches no suite is
documented in `select-e2e.sh` as a docs-only demo that stays ignored.

Verified with Bats and `hack/cozytest.sh` on
`hack/chainsaw-leak-guard_test.bats` (34 tests) and
`hack/select-e2e_test.bats` (65 tests; one case, "a path owned by
several sources counts as covered if any one is", fails on my machine on
main as well, from the local yq build, and does not touch this change).

Closes #4659


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

## Summary by CodeRabbit

* **Documentation**
* Clarified that Chainsaw suites may use `chainsaw-test.yaml` or `.yml`,
numbered step files, or either layout in a subdirectory.
* Updated guidance on suite selection and noted that disabled test files
are not selected or allowlisted.
* Identified the parked Kubernetes Proxmox suite and its workflow
availability.
* **Bug Fixes**
  * Suite validation now recognizes tests in the documented layouts.
* Validation rejects empty directories, parked test files, invalid
step-file names, and directories named like step files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 20:00:50 +03:00
Aleksei Sviridkin 013b166967 fix(e2e): read the KubernetesNodes pool release in the HelmRelease guard (#4970)
`cozy_guard_all_helmreleases` in
`hack/e2e-chainsaw/_lib/run-kubernetes.sh` reads the parent
`kubernetes-<test>` release and the addons selected by the
`kubernetes-<test>-` prefix. The worker pool is
`kubernetes-nodes-<test>-md0`, an Application HelmRelease the aggregated
apiserver stamps with `RetryOnFailure` like the parent, and it sits on
the node-join path through the talos-reconcile Job it renders. Its name
does not start with the addon prefix, so a failed action on it was never
read, on the passing path or in the failure-path report.

This reads it on both paths, with the `required` strategy check the
parent gets, since the apiserver stamps both. The pool name is derived
from the parent (`cozy_pool_helmrelease_name`), the way the addon prefix
is, so the two cannot drift. The failure-path report checks that the
release exists before reading it, with `get hr --ignore-not-found -o
name`: a run whose pool apply failed has no pool release (exit 0, empty
output), and the report says so instead of reporting a failed read. For
that run to reach the report at all, `run_kubernetes_test` now arms the
exit handler (`_tenant_failure_on_exit`, with
`CURRENT_TENANT_PARENT_HR`) right after the parent `Kubernetes` apply
instead of after the `KubernetesNodes` apply: before, a failing pool
apply exited under errexit with no handler installed, and the parent
went unclassified. The snapshot half still no-ops until the tenant
kubeconfig is set. A non-zero exit (a timeout, a kill, a Forbidden) is
reported as the failed read it is, through `_cozy_guard_read_failure`,
never as absence. The pool reads are gated on the report's own deadline
as well as the phase clock, like the addon listing.

Unit tests in `hack/run-kubernetes-remediation_test.bats` cover a
failing pool release on the passing path, a clean one (the composite
returns zero), an empty strategy on the pool, the report with and
without the pool release, a cut-off existence read, and a pool read past
the report budget; the ordering pin now also requires the handler to be
armed before the pool apply. The kubectl stub gained an existence read:
an `ABSENT` marker prints nothing and exits 0 under `--ignore-not-found`
(NotFound without it, as kubectl does), and the failure markers apply to
it as `<name>.exists`.

Verified with `hack/cozytest.sh` and Bats on
`hack/run-kubernetes-remediation_test.bats` (51 tests),
`hack/run-kubernetes-node-join_test.bats` and
`hack/remediation-guard.bats`.

Closes #4737


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

## Summary by CodeRabbit

* **Bug Fixes**
* Kubernetes release checks now include worker-pool releases alongside
the parent release and add-ons, and flag missing or invalid install
strategies and remediation failures.
* Failure reports distinguish worker-pool releases that are absent from
those whose status cannot be read. Existing releases are assessed
against the phase gate and report time budget; when the budget is
exhausted, skipped checks are identified.
* Failure handling now begins immediately after the parent resource is
applied, covering errors that occur while applying the worker pool.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 18:36:58 +03:00
Timofei Larkin 648dcd2aa3 docs(agents): steer agents away from Helm lookup
Nothing triggers a reconcile of a HelmRelease when an object one of its
templates looks up changes, so a lookup leaves the release on a stale
answer, and the project has worked to remove them. They keep spreading
because agents read the charts, find over a hundred lookups, and take
them as the convention. The guidance says they are not, names the one
accepted kind, and what to do instead.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-10-29 15:54:04 +01:00
IvanHunters 8d96c46994 fix(monitoring-agents): isolate merged log payload under log_processed
Fluent Bit's kubernetes filter runs with Merge_Log On but without
Merge_Log_Key, so a parsed JSON log line is merged into the record root.
An application log line can thereby forge the service fields that later
pipeline stages rely on for routing and tenant isolation
(kubernetes_namespace_name, kubernetes.labels, tenant, cluster,
log_source). Setting Merge_Log_Key log_processed nests the parsed
payload under a dedicated key so application content can no longer
override top-level service fields.

Applies to both the platform (system/monitoring-agents) and guest
(apps/kubernetes) Fluent Bit configurations.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-10-29 17:08:49 +03:00
Marian Koreniuk b61ad29783 fix(e2e): point the selector's suite rule back at the allowlist rule
The first commit says the two rules are cross-referenced so they move
together, but only lg_suite_has_test pointed at the selector. The
selector's suite enumeration comment now names lg_suite_has_test as well,
so a change to either side prompts the other.

The layout case also pins a chainsaw-test.yml that is a directory, the
other half of the type test: dropping that name from the find now turns
the case red.

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-29 13:59:43 +01:00
Marian Koreniuk 75bb70e58d fix(e2e): arm the tenant failure handler before the pool apply
The exit handler was armed after the KubernetesNodes apply, so a failing
pool apply exited under errexit with no handler installed: the parent
went unclassified, and the report's "pool release does not exist" line
described a run that could never reach it. Arm it right after the parent
Kubernetes apply instead. The snapshot half still no-ops until the
tenant kubeconfig is set, so the only change is that a failed pool apply
now gets the parent classified and the absent pool reported.

The ordering pin now also requires the handler to be armed before the
pool apply.

Assisted-by: LLM
Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
2026-10-29 12:34:22 +01:00
Aleksei Sviridkin 048c029e29 fix(e2e): keep the Proxmox tenant kubeconfig per run and remove it (#4971)
`hack/e2e-chainsaw/_lib/run-proxmox.sh` wrote the tenant's super-admin
kubeconfig to `$TMPDIR/proxmox-e2e-tenant.kubeconfig` with the process
umask and never removed it, so it outlived the run on the self-hosted
runner, world-readable under the usual umask. `dump_diagnostics` then
read whatever sat at that path, so a run that failed before
`fetch_tenant_kubeconfig` reported the previous run's tenant as its own.

- The file now lives under `RUNNER_TEMP`, which the runner exports into
every step and empties between jobs; `COZY_PROXMOX_WORKDIR` overrides it
for a local run, and `TMPDIR` is the last fallback. The workflow sets no
path of its own: the `runner` context is not available in job-level
`env`.
- `fetch_tenant_kubeconfig` writes it under `umask 077` in a subshell,
since the workflow's `umask 077` covers only the staging step.
- `apply-tenant`, the suite's first step, removes a leftover before
doing anything else, so diagnostics in this run can only see this run's
file.
- The suite's `finally` removes it, before the `keep_tenant` exit, and
the workflow's "Remove staged credentials" step removes it again for a
run killed before `finally`.

Verification: `sh -n` and shellcheck on the script; actionlint on
`e2e-proxmox.yaml` (only the custom `proxmox` runner label is reported,
as on main). `hack/proxmox-e2e-fixture_test.bats` has three new cases
with a stub `kubectl`: the fetched file has no group or other bits under
`umask 022`, `apply-tenant` removes a planted leftover, and the suite's
`finally` (run out of the Chainsaw file on the `keep_tenant` path)
removes the file, so `diagnostics` after it prints no tenant section.
Each goes red with its line (`umask 077`, the leftover `rm`, the
`finally` `rm`) removed. All `hack/proxmox-*.bats` pass (fixture 9,
teardown contract 8, step1b 4, talos template 12), also under
`hack/cozytest.sh`. Not run on a Proxmox stand: the only stand I have
cannot dispatch the workflow at the moment, so the workflow steps
themselves are unexercised beyond actionlint.

Closes #4806


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

## Summary by CodeRabbit

* **Chores**
* Proxmox end-to-end test runs store tenant credentials in a temporary
work directory and create kubeconfig files with restricted permissions.
* Stale tenant kubeconfig files are cleared before a new tenant fixture
is applied. Cleanup removes the file even when tenant data is retained
or a run is interrupted, preventing credentials from appearing in later
diagnostics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 11:31:31 +03:00
Aleksei Sviridkin 3abdff5900 fix(kubernetes-nodes): refuse a partial resources spec on every pool (#4973)
The `resources.cpu`/`resources.memory` pair check in `nodegroup.yaml`
ran only when an `instanceType` was set, while the field description
(values.yaml, README, schema, the KubernetesNodes OpenAPI schema)
promised a render-time error for a partial spec on any pool. Without an
instancetype a cpu-only pool renders a VM with no memory, which KubeVirt
refuses when the Machine is created, and a memory-only pool silently
sizes the guest at KubeVirt's default of one vCPU.

- The check now runs on every kubevirt pool. The proxmox branch already
required both, so this is the kubevirt half of one rule, and the field
descriptions are true as written (no doc change). On a pool with no
`instanceType` the message ends in "or neither and set instanceType", so
it does not send the user straight into the next refusal.
- A kubevirt pool with neither an `instanceType` nor both resources
fails at render with a message naming both, the way `vm-instance` does.
No such pool could have passed admission (no memory), so this refusal
breaks nothing that runs today. This is the decision the issue asked
for; if the gate should stay narrower, this commit can be dropped on its
own.
- **Upgrade impact.** One shape that renders on main stops rendering:
`instanceType: ""` with only `resources.memory`. On main it gets
`domain.memory.guest` and no `domain.cpu`, so KubeVirt runs the worker
with one vCPU; with this PR it fails with the partial-spec message, and
the pool's HelmRelease sits in a failed upgrade until the values are
edited. To keep the current size such a pool needs `resources.cpu: "1"`.
Adding that line renames the machine template (for the test values,
`kubernetes-myk8s-md0-28d812` becomes `kubernetes-myk8s-md0-1ce103`, the
same name on main and on this branch), so the pool's workers are
replaced once. cpu-only with `instanceType: ""` is not in this class: it
renders a VM with no memory, which KubeVirt refuses, so it never ran.
- `cores:` printed an unquoted (float64) core count of one million or
more in exponent form (`1e+06`). It is printed as an integer now, with
the same bytes for every smaller whole number.

Tests: `tests/resources_partial_test.yaml` covers both halves with and
without an instancetype, the no-sizing case, both fields set, and the
float64 formatting. The existing unquoted-zero case in
`resources_cpu_test.yaml` now reads the field as unset on an
instancetype-sized pool (with a `kubernetesProvider` stub for the
lookup), and zero beside memory is pinned as a partial spec.

Byte stability, checked by snapshotting the `KubevirtMachineTemplate` on
main for nine spellings of `resources.cpu` and replaying the snapshots
on this branch: `"2"`, `2`, `2.0`, `"2.0"`, `"+2"`, `"1e1"`, `"010"`,
`999999` render identical bytes and template names. Only unquoted
`4294967295` changes, from `cores: 4.294967295e+09` to `cores:
4294967295`.

Verified with `helm unittest .` in `packages/apps/kubernetes-nodes` (38
suites, 233 tests, 12 snapshots), and on a trial merge with current main
(39 suites, 237 tests).

Closes #4546. The leading-zero inconsistency from the issue's comment is
addressed separately, together with #4554.

### Release note

```release-note
fix(kubernetes-nodes): a partial `resources` spec (only `cpu` or only `memory`) is now refused at render time on every kubevirt worker pool, not only on a pool with an `instanceType`, and a pool with neither an `instanceType` nor both resources is refused as well. Action required: a pool with `instanceType: ""` and only `resources.memory` rendered before (KubeVirt ran it with one vCPU) and now fails to upgrade; add `resources.cpu: "1"` to keep its current size. Adding that line renames the pool's machine template, so its workers are replaced once. Whole-number core counts of one million or more are now printed as integers instead of in exponent form.
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* KubeVirt worker pools now require either an instance type or both CPU
and memory values. Configurations with only one resource or no sizing
information are rejected.
* Whole-number CPU values render without exponent notation, including
values written with a decimal fraction.
* An unquoted zero CPU value is treated as unset when an instance type
is provided; without an instance type, pairing zero CPU with memory
fails validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 11:22:48 +03:00
Aleksei Sviridkin 60d1f53e82 fix(kubernetes-nodes): plan autoscaler capacity on the cores the VM actually gets (#4974)
On the kubevirt substrate `resources.cpu` is written into
`domain.cpu.cores` as spelled and decoded as YAML, so `"010"` gives the
VM eight cores, while the cluster-autoscaler capacity annotation parsed
the same string as decimal ten
(`capacity.cluster-autoscaler.kubernetes.io/cpu: "9000m"` on a node that
offers 7000m). This is the inconsistency reported in #4554 and in the
comment on #4546.

- The YAML-decoded value the range check already computes is kept, and
on kubevirt the capacity annotation and the reserved-CPU check start
from it.
- The kubelet reservations are not touched. The worker machineconfig
(`_talosconfigtemplate.tpl`) sizes them from the raw string read as a
decimal, and the TalosConfigTemplate name is a hash of that config, so
reading the decoded value there would roll every leading-zero pool once.
Instead the annotation subtracts the reservations the machineconfig
really carries from the cores the VM really has: for `"010"` that is
8000m − 2 × 500m = `"7000m"`, the node's real allocatable, while `"8"`
keeps `"7200m"` (2 × 400m). Tests pin both sides: the annotation and the
bootstrap `configRef` name in `resources_cpu_test.yaml`, and
`systemReserved.cpu` / `kubeReserved.cpu` in the machineconfig in
`talos_worker_config_test.yaml`.
- No pool rolls. Checked by rendering `nodegroup.yaml` on main and on
this branch for nine spellings (`"2"`, `2`, `2.0`, `"2.0"`, `"+2"`,
`"1e1"`, `"010"`, `999999`, `4294967295`): the only difference is the
capacity annotation of the `"010"` case, so the
`KubevirtMachineTemplate` name and the TalosConfigTemplate `configRef`
stay as they are. The machineconfig template itself is not changed.
- Proxmox parses the quantity as decimal for the machine template and
for the reservations alike, so it never had the inconsistency and is
unchanged (pinned by a test).
- The `resources` description says the value is decoded as YAML, what
that means for a leading zero, and that the reservations still read it
as a decimal. README, schema, the RD and the Go type are regenerated
from it.

This closes #4554 without the migration and the render-time refusal it
proposes. A refusal at render breaks a stored pool on its next upgrade,
a pattern in the OpenAPI schema breaks every update of an existing CR,
and a platform-wide migration over every `KubernetesNodes` and parent
`nodeGroups` entry is a lot of blast radius for a spelling nobody has
reported in a live cluster. With this change the autoscaler plans for
the node the VM really is. What is left of a leading zero is the octal
reading itself and reservations sized from the decimal value, and both
are documented. The unquoted zero of the issue is already harmless: it
is falsy, counts as unset, and renders the same bytes as an absent
field. If a migration is still wanted, please reopen.

Verified with `helm unittest .` in `packages/apps/kubernetes-nodes` (38
suites, 233 tests, 12 snapshots; pointing the annotation back at the
decimal string turns the `"010"` case red with `9000m`), `make generate`
(no diff), and a trial merge with #4973 (clean, 243 tests green).

Closes #4554


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

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

## Summary by CodeRabbit

* **Bug Fixes**
* KubeVirt VM sizing and autoscaler capacity now reflect YAML-decoded
CPU values. For example, `"010"` represents eight VM cores, while
kubelet reservations interpret it as decimal ten. `"8"` and `"10"` yield
`7200m` and `9000m` of autoscaler capacity, respectively. Proxmox
continues to interpret `"010"` as ten cores, yielding `9000m` of
capacity.
* **Documentation**
* Clarified explicit worker CPU and memory sizing behavior, including
how leading-zero CPU values affect VM sizing, reservations, and capacity
calculations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 11:22:40 +03:00
Aleksei Sviridkin 44848b9c9d fix(kubernetes): run Proxmox CCM v0.16.1 and pin the Proxmox images by digest (#4917)
## What this PR does

`v1.7.0-rc.1` and `release-1.7` ship the Proxmox
cloud-controller-manager as
`ghcr.io/sergelogvinov/proxmox-cloud-controller-manager:v0.15.0`, a bare
tag. That tag was re-pushed upstream on 2026-09-27: it used to name the
2026-08-06 build (`sha256:fbc553921145…`) and now names
`sha256:d81702998ae1…`, a binary whose gitVersion is `0.15.0` without
the leading `v`. component-base v0.37 parses that string in an `init()`
and panics:

```
panic: version string "0.15.0" doesn't match expected regular expression: "^v(\d+\.\d+\.\d+)"
```

The CCM Deployment crash-loops, no new Node loses
`node.cloudprovider.kubernetes.io/uninitialized`, and workloads on new
workers stay Pending. On a live Proxmox cluster this went unnoticed for
five days: the existing Nodes were already initialized, and the failure
only showed when the next new worker came up. Every fresh install pulls
the broken build, so as it stands v1.7.0 would be the first stable
release with the Proxmox substrate, shipping a CCM that cannot start.

Two commits, both cherry-pick onto `release-1.7` unchanged:

1. **`fix(kubernetes)`**: run v0.16.1, pinned as tag plus digest in
`images/proxmox-cloud-controller-manager.tag`. That is the same shape
the chart already uses for the KubeVirt cloud provider. v0.16 builds
with goreleaser and stamps the version with its `v`. The config the init
container writes still parses: the cluster entry moved to
go-proxmox-pool's `ClusterConfig` with the same `url`, `insecure`,
`token_id`, `token_secret` and `region` keys, and `features.provider:
capmox` still writes `proxmox://<SMBIOS UUID>`, the providerID capmox
puts on the Machine.
2. **`fix(kubernetes, proxmox-csi)`**: pin the CSI driver from the same
registry at its current v0.20.0, with no version change. The
management-side controller goes through
`images/proxmox-csi-controller.tag`. The tenant-side node plugin goes
through `repository` + `tag` in
`packages/system/proxmox-csi/values.yaml`, which also puts it in
`hack/lib/image-refs.sh` and so in front of the release multi-arch gate.
Pinning takes the version out of the vendored chart's hands, so
`hack/proxmox-csi-driver-pin_test.bats` now fails unless the chart's
`appVersion` and both pins name the same release.

All three digests are OCI indexes for `linux/amd64` and `linux/arm64`.
The sig-storage sidecars stay on tags, as they do for the KubeVirt CSI
driver: registry.k8s.io publishes through image promotion, which does
not move a tag.

### Testing

- `helm unittest`: `packages/apps/kubernetes` 31 suites / 346 tests,
`packages/apps/kubernetes-nodes` 37 / 223, all passing.
- bats: the new test, plus `image-pin-consistency`,
`promote-rewrite-tags_test`, `overlay-main-images_test`,
`nightly-mirror_test`, `promote-retag_test`, `stitch-multiarch_test`,
`bats-strict-setup`, `bats-runner-coverage`: 107/107. The new test was
checked to fail on a node tag at another version.
- `git cherry-pick -x` of both commits onto `release-1.7` (315b335cf)
applies cleanly, and the Proxmox unit suites and the new bats test pass
there.
- v0.16.1 compatibility was checked against its source, as described
above. It has also run on a live Proxmox cluster, including the
initialisation of a new worker; the results are in the comment below.

### Screenshots

Not a UI change.

### Downstream repositories

The diff touches no app `values.schema.json` or defaults, adds or
renames no package, and moves nothing under `hack/`.

- [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 Proxmox cloud-controller-manager now runs v0.16.1, and the Proxmox CCM and CSI driver images are pinned by digest. The v0.15.0 tag referenced before was re-pushed upstream to a build that panics on start, which left every new Proxmox worker with the uninitialized taint.
```
2026-10-29 11:20:22 +03:00
Aleksei Sviridkin 040ae377ae test(kubernetes): name the Proxmox fixture's node pool <cluster>-<pool> (#5000)
## What this PR does

The Proxmox suite (`hack/e2e-chainsaw/kubernetes-proxmox/`) has now run
for the first time on a real self-hosted stand: a dedicated Cozystack
v1.7.0-rc.1 management cluster on Proxmox VE, with the runner CageForge
offered to host in cozystack/community#98. The suite failed before it
reached the hypervisor, because the fixture and the chart disagree on
what a node pool may be called.

`kubernetes-nodes` names its release `kubernetes-nodes-<CR name>` and,
since fd14126be, refuses to render one that does not start with
`kubernetes-nodes-<spec.cluster>-`
(`packages/apps/kubernetes-nodes/templates/_helpers.tpl`). The fixture,
added later in 7500a9fba, names its `KubernetesNodes` `md0`. On the
stand that gave:

```
Helm install failed for release tenant-e2eproxmox/kubernetes-nodes-md0 ...:
kubernetes-nodes: release name "kubernetes-nodes-md0" must start with "kubernetes-nodes-k8s-e2e-"
— name the KubernetesNodes CR <cluster>-<pool> (cluster="k8s-e2e")
```

No `Machine` was ever created. The `tenant-and-cluster` step passed its
`ProxmoxCluster` assert in 20 seconds and then waited out the 20-minute
`Machine` assert.

- The fixture names the pool `k8s-e2e-md0`. Nothing else in the suite,
`run-proxmox.sh` or the workflow refers to the pool by name.
- `hack/proxmox-e2e-fixture_test.bats` gets a case that reads
`metadata.name` and `spec.cluster` of the fixture's `KubernetesNodes`
and fails unless the name is `<cluster>-<pool>`. It is POSIX sh like the
rest of the file, so `hack/cozytest.sh` can still run it.

### Testing

- `bats hack/proxmox-e2e-fixture_test.bats` (bats 1.14.0, the CI
commit): 7/7. The new case was checked to fail against the old fixture:
`FAIL: KubernetesNodes md0 must be named k8s-e2e-<pool>`.
- `hack/check-commit-trailers.sh upstream/main..HEAD`: OK.
- Lane, before the fix (fork `main` @ aed7fa896):
https://github.com/themoriarti/cozystack/actions/runs/39402535098. Red
on the `Machine` assert, as above; the teardown removed the tenant and
left no VM on the hypervisor.
- Lane, with this commit:
https://github.com/themoriarti/cozystack/actions/runs/39442983145. The
pool release `kubernetes-nodes-k8s-e2e-md0` installs, the `Machine` is
created, capmox clones the worker from the template and boots it, and
the worker joins the tenant and goes `Ready` (Talos v1.13.6). The run is
still red, one step later and for a known reason: the tenant's CCM
crash-loops on `pccm:v0.15.0` with `panic: version string "0.15.0"
doesn't match expected regular expression: "^v(\d+\.\d+\.\d+)"`, so the
node keeps `node.cloudprovider.kubernetes.io/uninitialized` and no
providerID, and the `Machine` assert reports `Provisioned`, expected
`Running`. That is #4917. The teardown removed the tenant, the worker VM
and its cloud-init ISO.

Two earlier runs on the same commit, 39406900563 and 39409765857,
stopped at stand configuration rather than at this repository: capmox's
token could not see an ISO storage, and the stand's `publishing.host`
did not resolve from the fixture's public resolvers. Both are fixed on
the stand. They are listed below because the lane does not say a stand
needs them.

### Found on the way, not changed here

- **What a stand must provide**, beyond the `PROXMOX_*` variables:
- an `iso`-content storage on which capmox's token holds
`Datastore.AllocateTemplate` (to upload the cloud-init ISO) and
`Datastore.Allocate` (to delete it with the VM). go-proxmox uploads to
the first `iso` storage the token can see; without delete rights the
`Machine` never finishes deleting.
- a `publishing.host` whose `<cluster>.<tenant>.<host>` resolves through
the fixture's own resolvers (`8.8.8.8`, `1.1.1.1`) to a root ingress
with SSL passthrough: on the Proxmox substrate that name is the tenant's
`controlPlaneEndpoint`.
- **`COZY_PVE_ADDRESSES/GATEWAY/PREFIX/BRIDGE/POOL`** are not passed by
the workflow. Unset, they default to `vmbr50` and `10.0.0.180-185`. This
stand sets them in the runner's environment.
- **The fixture's `templateTags`** (`capmox-template, cozystack, talos,
talos-v1.13.6`) are not the set that `hack/proxmox-talos-template.sh`
writes (`cozystack, schematic-…, talos, talos-v…`). capmox matches tags
as a set, so a template built by the script is not selected. This stand
tags its template to match the fixture.
- **Teardown reports success once the CRs are deleted.** It does not
wait for the VMs to be gone; on this stand a worker VM outlived its
`Machine` for hours while capmox retried a forbidden delete.

The CCM fix itself is #4917. Once a release carries it, this lane should
get past the `Machine` assert.

The lane stays manual. It has no `push` or `pull_request` trigger, and
its nightly schedule is a no-op unless `PROXMOX_NIGHTLY` is set, which
it is not on this stand. The runner is registered to a fork whose only
collaborator is its owner. Running it from cozystack/cozystack needs a
runner group restricted to this workflow and a protected environment,
which is a follow-up for community#98, not this PR.

### Screenshots

Not a UI change.

### Downstream repositories

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

The diff touches one fixture under `hack/e2e-chainsaw/` and one bats
file. It moves or renames nothing under `hack/`, changes no make target
and no node prerequisite.

### Release note

```release-note
NONE
```


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

## Summary by CodeRabbit

- **Bug Fixes**
- Updated the Kubernetes node pool fixture to use the required
cluster-prefixed naming convention, improving compatibility with
Kubernetes node validation.

- **Tests**
- Added validation to confirm that Kubernetes node fixtures include the
required cluster reference and use the expected cluster-based name
format.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 11:19:21 +03:00
myasnikovdaniil 5c564f9154 fix(release): keep release bodies within GitHub's size limit (#5142)
## What this PR does

Finalize sets release body after the stable tag, `api/apps/v1alpha1` tag
and `release-X.Y` fast-forward already exist. Body that GitHub refuses
fails the "Publish draft release" step at that point and leaves release
a draft, and rc-to-stable image retag with `cozy-installer` chart
publish never run. GitHub's web UI cut the v1.7.0-rc.2 changelog at
about 125000 characters, v1.7.0 changelog is 259,026.

Release can now carry short hand-written `docs/releasenotes/vX.Y.Z.md`
that becomes its body, changelog stays the full record. Finalize and
`update-releasenotes.yaml` take release notes if they exist and are not
blank, otherwise changelog.

Both hold the body to 100000 bytes. Bytes are never fewer than
characters and the limit sits 25 percent under the cut seen in web UI.
Exact GitHub limit and its answer to an oversized body don't matter.

Over the limit finalize publishes one-line link to the changelog on the
tag, with a warning in the run log. v1.6.0 changelog is 97,186 bytes, so
a next release near that size without release notes gets the link. Sync
never writes the link, it warns and leaves published body alone, because
`tags.yaml` backstop brings changelog to `main` without release notes
and sync would replace them.

`update-releasenotes.yaml` also runs on pushes touching
`docs/releasenotes/v*.md`. `docs/release.md` describes the rule,
including that release notes have to reach `main` too.

Release without release notes and with changelog under 100000 bytes is
published as before, byte for byte.

## Tests

`hack/release-changelog-behaviour.bats` runs real `script:` of both
workflow steps under node on fixture trees. Finalize cases are release
notes over changelog, blank release notes, changelog only, neither file,
100000 bytes accepted and 100001 replaced by link, and 40000 three-byte
characters (120000 bytes) counted as over. It also runs finalize over
every shipped changelog, so each one at or under the limit publishes
byte for byte. Sync makes the same choice, never updates release whose
body would be over the limit, and leaves up-to-date and missing ones
alone.

`hack/release-changelog-contract.bats` pins new trigger path and the
100000 constant.

### 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(release): a release can carry short docs/releasenotes/vX.Y.Z.md that is published as its GitHub release body, changelog stays the full record. Release body over 100000 bytes is published as a link to the full changelog on the tag, so publishing a release with very long changelog no longer fails after its tags exist, and the release notes sync leaves such a body alone.
```


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

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

## Summary by CodeRabbit

* **Release Management**
* Release publication and subsequent updates now prefer non-empty
versioned release notes, falling back to the changelog when needed.
* If neither source contains content, the existing draft body is
preserved. Bodies exceeding 100,000 bytes are replaced with a link to
the tagged changelog during publication; updates are skipped.
* Changes to either release notes or changelogs now trigger release-body
synchronization.
* **Documentation**
* Updated release instructions to describe the content selection, size
limit, and synchronization behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-29 12:48:43 +05:00
Marian Koreniuk f7913329b2 fix(kubernetes-nodes): plan autoscaler capacity on the cores the VM actually gets
On the kubevirt substrate resources.cpu is written into domain.cpu.cores
as spelled and decoded as YAML, so "010" gives the VM eight cores, while
the cluster-autoscaler capacity annotation parsed the same string as
decimal ten. The autoscaler then planned 9000m on a node that offers
7000m.

Keep the YAML-decoded value the range check already computes and start
the capacity annotation and the reserved-CPU check from it on kubevirt.
The kubelet reservations stay as they are: the worker machineconfig
sizes them from the decimal string, and the TalosConfigTemplate name is
a hash of that config, so changing them would roll every leading-zero
pool once. The annotation therefore subtracts the reservations the
machineconfig really carries (2 x 500m for "010") from the cores the VM
really has, which is the node's real allocatable. Neither template name
changes for any spelling, so no pool rolls. Proxmox parses the quantity
as decimal on both sides and is unchanged. The field description says
the value is decoded as YAML and that the reservations still read it as
a decimal.

This closes #4554 without the migration and the render-time refusal it
proposes: a refusal breaks stored pools on their next upgrade, a schema
pattern breaks updates of existing CRs, and a platform-wide migration of
every KubernetesNodes and nodeGroups entry is a lot of blast radius for a
spelling nobody has reported in a live cluster.

Closes #4554

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-28 21:04:27 +01:00
Marian Koreniuk cd983d416e fix(e2e): take the selector's type test in the allowlist scan and pin its regex
lg_suite_has_test dropped every directory with `! -type d`, but the
selector keeps a chainsaw-test.y*ml whatever its type, so Chainsaw can
fail loudly on a directory by that name. An allowlisted suite in that
shape failed validation and stopped the run before Chainsaw reported the
real error. Use the selector's own `\( ! -type d -o -name
chainsaw-test.yaml -o -name chainsaw-test.yml \)`, which still drops a
directory with a step-file name.

The step-file regex behind the find glob was never reached by a test:
install-01.yaml is already filtered by the glob. 1a-install.yaml passes
the glob and not the regex, so it pins the regex; a directory named
chainsaw-test.yaml and one named 01-install.yaml pin the type test.

The README said the runner recognises the same three layouts. It
recognises none: it hands every top-level directory to Chainsaw.

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-28 18:53:07 +01:00
Marian Koreniuk 3d97e912b4 fix(e2e): drop the job-level runner.temp and pin the tenant kubeconfig handling
jobs.e2e.env set COZY_PROXMOX_WORKDIR from runner.temp, but the runner
context is not available in job-level env, so GitHub would refuse the
whole workflow at load time and the Proxmox lane would stop starting.
actionlint reports it as `context "runner" is not allowed here`.

Drop the variable. run-proxmox.sh already falls back to RUNNER_TEMP,
which the runner exports into every step, and the cleanup step removes
the file from there.

Pin the behaviour the previous commit added, none of which a test
covered: the fetched kubeconfig carries no group or other bits under
umask 022, apply-tenant removes a file an earlier run left, and the
suite's finally removes the file on the keep_tenant path, so diagnostics
after it prints no tenant section. Each case goes red with its line
removed.

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-28 18:49:12 +01:00
Marian Koreniuk 4d7d336efc fix(e2e): tell a failed pool existence read from an absent pool release
The failure-path report checked the pool release with `get hr -o name`
and discarded the exit status and stderr, so a timeout, a kill, a
Forbidden or a refused connection all printed that the run ended before
the worker pool was applied. That is the conflation the guard wrapper
exists to refuse, on the path where a slow apiserver is most likely.

Read it with --ignore-not-found instead: exit 0 with empty output is an
absent release, exit 0 with a name is one to classify, and any non-zero
exit is a failed read named through _cozy_guard_read_failure.

The pool reads also skipped the report's own deadline and checked only
the phase clock, which says yes on every path that opens no phase.
Check the deadline before the existence read, like the listing and the
addons do, and name what was not read.

The stub's ABSENT marker now answers the flag the way kubectl does. New
cases pin the failed read and the deadline; the two budget cases are
re-timed for the extra clock reading.

Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
Assisted-by: LLM
2026-10-28 18:45:50 +01:00
Marian Koreniuk 28d9d352e1 test(kubernetes): name the Proxmox fixture's node pool <cluster>-<pool>
kubernetes-nodes names its release kubernetes-nodes-<CR name> and refuses
to render one that does not start with kubernetes-nodes-<spec.cluster>-
(_helpers.tpl, since fd14126be). The Proxmox fixture named its
KubernetesNodes "md0", so helm install of the pool failed, no Machine was
ever created, and the first real run of the lane on a self-hosted stand
timed out on the Machine assert after 20 minutes.

Name the pool k8s-e2e-md0 and add a fixture test that fails on any name
the chart would refuse.

Assisted-by: LLM
Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
2026-10-28 18:44:04 +01:00
Marian Koreniuk 780b068df0 fix(kubernetes, proxmox-csi): pin the Proxmox CSI driver images by digest
The CSI controller in the management cluster was referenced by the bare
tag ghcr.io/sergelogvinov/proxmox-csi-controller:v0.20.0, and the node
plugin in the tenant by the vendored chart's default, its appVersion as
a bare tag. That registry is the one whose proxmox-cloud-controller-
manager:v0.15.0 was re-pushed to a build that panics, so a tag there is
not a statement of what runs.

Both now carry the v0.20.0 digest: the controller through
images/proxmox-csi-controller.tag, like the cloud-controller-manager, and
the node plugin through repository and tag in the proxmox-csi values,
which also makes it visible to hack/lib/image-refs.sh and therefore to
the release multi-arch gate. Both digests are indexes for linux/amd64
and linux/arm64.

Pinning takes the version out of the vendored chart's hands: a chart
bump would bring a new appVersion while both pins kept naming the old
release, and the controller and the node plugin would drift apart from
the templates and from each other without anything noticing.
hack/proxmox-csi-driver-pin_test.bats fails unless the chart's
appVersion and both pins name the same release.

The sig-storage sidecars stay on tags, as they do for the KubeVirt CSI
driver: registry.k8s.io publishes them through image promotion, which
does not overwrite a tag.

Assisted-by: LLM
Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
2026-10-28 18:38:35 +01:00