Commit Graph
1680 Commits
Author SHA1 Message Date
Andrei KvapilandClaude 21ee665585 fix(cozystack-api): pin the image to the build that ships the SecurityGroup API
#2922 added the sdn.cozystack.io SecurityGroup API to cozystack-api, but
main's committed image is still the v1.5.0 release, which predates it — the
new build was only pinned transiently inside #2922's own E2E and never
committed. As a result every PR that does not itself rebuild cozystack-api
installs the old apiserver, so `kubectl get securitygroup` returns
"the server doesn't have a resource type" and the securitygroup /
serviceexposure e2e tests fail on unrelated PRs.

Pin cozystack-api to the current main-HEAD build that carries the API
(image mirrored to ghcr from the #2922 build). Stopgap until a release
re-pins it through the normal build.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-07-01 13:35:56 +02:00
Aleksei Sviridkin 03435dcc50 fix(flux): tune source-watcher concurrency + CPU to unblock platform install under load (#3163)
## What this PR does

`source-watcher` is the Flux controller that turns each `PackageSource`
into an `ExternalArtifact` (one `ArtifactGenerator` per source, ~100
builds on the management cluster during install). The
`cozystack-platform` HelmRelease blocks until all of those artifacts are
Ready, so source-watcher's throughput sits on the install's critical
path.

It was the only Flux controller left without concurrency/CPU tuning. In
the bootstrap all-in-one pod it ran on the binary default
(`--concurrent=10`) with the shared 100m CPU request; in the production
`FluxInstance` it was absent from the reconciler patch that source-,
kustomize- and helm-controller already receive. Under load its
per-artifact builds finish at staggered times, leaving a random tail of
not-yet-Ready artifacts that times out the platform install.

A failed run's diagnostics show the shape of this: the
`cozystack-platform` HelmRelease timed out with `timeout waiting for:
PackageSource/… status: 'InProgress'` (a run-to-run varying tail — e.g.
`bucket-application`, `linstor-gui`, `capi-provider-core`), while its
~200 ExternalArtifacts reached Ready in a staggered trickle spread over
~60 seconds rather than together. That staggered completion of
independent per-artifact builds is the signature of a throughput-limited
builder, not a single all-or-nothing pull.

This raises source-watcher to `--concurrent=20` in both install paths:

- **Bootstrap all-in-one pod**
(`internal/fluxinstall/manifests/fluxcd.yaml`, regenerated from
`flux-aio.cue` + the `flux-aio` Makefile): `--concurrent=20` plus a 500m
CPU request. The AIO pod sets no CPU limits, so the request is what
drives source-watcher's proportional CPU share when the node is
saturated.
- **Production `FluxInstance`** (`packages/system/fluxcd/values.yaml`):
source-watcher is added to the shared reconciler patch, so it now gets
the same `--concurrent=20`, `--requeue-dependency=5s` and 2000m/2048Mi
limits as the other tuned controllers.

The `flux-aio` module exposes a per-controller resources override for
the watcher (set via CUE) but no args override, so the bootstrap
`--concurrent` bump is a guarded post-render patch in the Makefile; the
embedded manifest is the product of that pipeline. source-watcher's
support for both `--concurrent` and `--requeue-dependency` was confirmed
against its `cmd/main.go`, and the production `op: replace` on
`resources/limits` is safe because the distribution ships source-watcher
with a `resources.limits` block (same shape as source-controller).

Optional follow-up, intentionally left out to keep this focused on the
root cause: if the artifact-build tail still brushes the 10-minute
platform HelmRelease timeout under load, the installer's HelmRelease
install/upgrade timeout values give a safety margin.

### Release note

```release-note
fix(flux): tune source-watcher concurrency and CPU so the platform install does not time out under load
```


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

## Summary by CodeRabbit

* **New Features**
* Increased the source watcher’s concurrency to improve throughput and
better handle larger workloads.
* Added higher CPU and memory settings for the watcher to support more
stable operation under load.

* **Bug Fixes**
* Updated deployment patching so the watcher receives the same
configuration adjustments as other controllers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-01 13:20:55 +03:00
Aleksei Sviridkin 41ff94eca9 fix(flux): tune source-watcher concurrency and CPU under load
source-watcher reconciles one ArtifactGenerator per PackageSource and
builds the resulting ExternalArtifacts. On the management cluster that is
~100 artifact builds during install, and the platform HelmRelease blocks
until all of them are Ready.

It was the only Flux controller left untuned. In the bootstrap all-in-one
pod it ran without an explicit --concurrent (binary default 10) and with
the shared 100m CPU request; in the production FluxInstance it was missing
from the --concurrent / CPU patch that source-, kustomize- and
helm-controller already receive. Under load its per-artifact builds finish
at staggered times, leaving a random tail of not-yet-Ready artifacts that
times out the platform install.

Raise source-watcher to --concurrent=20 in both deployments. In the
bootstrap all-in-one pod also give it a 500m CPU request; the pod has no
CPU limits, so the request is what drives its CPU share under contention.
In the production FluxInstance add it to the shared reconciler patch, so it
now receives the same --requeue-dependency=5s and 2000m/2048Mi limits the
other tuned controllers already have.

The flux-aio module exposes a resources override for the watcher (set via
CUE) but no args override, so the bootstrap --concurrent bump is a
post-render patch in the Makefile.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-01 07:42:33 +03:00
Aleksei Sviridkin f7271b8011 fix(velero): add startupProbe so slow startup does not crashloop the install gate
Velero binds its metrics/health endpoint (:8085, http-monitoring) only
after the server loads its plugins and connects to the API server. Under
a heavy parallel platform install that can take minutes, but the upstream
liveness probe begins checking /metrics at initialDelaySeconds=10 and
kills the container after failureThreshold=5 (~130s total) before :8085
is bound. The kubelet SIGTERMs velero (exitCode 0/Completed), it
BackOff-restarts, and never escapes the loop inside the install-gate
window. Because backupstrategy-controller hard-depends on velero, this
blocks the gate.

Add a startupProbe to the velero server container. While it is failing
the kubelet holds off liveness and readiness entirely, granting
failureThreshold * periodSeconds = 30 * 10s = 300s of startup grace, then
hands over to the unchanged, tight upstream liveness (10s / 30s / 5) for
steady-state crash detection. This is strictly better than loosening
liveness, which would also blunt steady-state detection.

The upstream velero chart (12.0.3) does not render a startupProbe, so the
package patches the deployment template via patches/add-startup-probe.patch
(re-applied after re-vendor by the Makefile update target, matching the
seaweedfs/ingress-nginx convention) and supplies the startupProbe through
the package values override. Pin the startupProbe budget and the unchanged
liveness with a helm-unittest regression guard.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-01 06:04:22 +03:00
Andrei KvapilandClaude d6f66d2dcd fix(vertical-pod-autoscaler): stop the updater OOM + VPA-for-VPA eviction churn
Two root causes of e2e install flakiness, both in the VPA package:

1. The updater memory limit was 110Mi. The updater watches every pod in
   the cluster, so on a full cozystack install it OOM-kills, which trips
   the Deployment's ProgressDeadlineExceeded and makes Flux fail the
   HelmRelease 'early due to stalled resources' - cascading NotReady to
   monitoring, velero, etcd-operator and everything downstream. Raise the
   limit to 512Mi (request 200Mi); the recommender is already unlimited.

2. The two VPA-for-VPA VerticalPodAutoscaler objects ran updateMode: Auto,
   which evicts the VPA recommender(s) during install and adds pod churn on
   an already busy cluster. Switch to Initial, matching the existing
   monitoring/etcd VPA->Initial fixes.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-01 03:27:15 +03:00
Алексей Артамонов d988b3e8c8 fix(apiserver): read tenant pool quota usage via direct client and grant RBAC
parentPoolUsage listed ResourceQuotas through the aggregated apiserver's cached
client, which lazily starts a cluster-wide ResourceQuota informer. cozystack-api
has no RBAC to list resourcequotas, so the informer never synced and tenant
create/update of a bounded child hung until the request timed out (observed on a
cluster). Read quota usage through the direct (uncached) watch client instead,
scoped to the pool's member namespaces, so no cluster-wide informer is started,
and grant cozystack-api get/watch/list on resourcequotas for those reads.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Алексей Артамонов <alexeyartamonov1987@gmail.com>
2026-06-30 16:12:17 +02:00
Andrei KvapilandClaude 844deb170f feat(controller): enforce recursive tenant quotas at runtime
Add a tenant quota reconciler to cozystack-controller that realises the
hierarchical (sub-tree) quota model at runtime. Each tenant's declared
quota is the budget for its whole sub-tree: a child that declares its own
quota carves a fixed slice out of the parent budget, while a child without
a quota shares the parent's remaining pool.

The controller maintains a per-namespace 'tenant-quota-allocated'
ResourceQuota (Kubernetes enforces the most restrictive quota, so it binds
without fighting Flux over the chart-rendered 'tenant-quota'), aggregating
sub-tree usage so a pool's members collectively stay within the budget. A
--tenant-quota-buffer-percent flag temporarily inflates pool budgets so
workloads already over a freshly-introduced quota keep running during
rollout; overcommitted pools emit a QuotaOvercommitted event.

The model and the selector-spans-namespaces/aggregated-usage approach are
adapted from OpenShift's ClusterResourceQuota controllers (openshift/api,
library-go, cluster-policy-controller, apiserver-library-go), Apache-2.0,
Copyright Red Hat, Inc. and the OpenShift Authors.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
2026-06-30 16:12:17 +02:00
Aleksei Sviridkin eab9b034f6 fix(seaweedfs): enforce read-only bucket access by bumping cosi-driver to v0.3.1 (#3136)
## What this PR does

The SeaweedFS COSI driver shipped at `v0.1.2` hardcodes read-write S3
actions (`Read,Write,List,Tagging`) for every BucketAccess and never
reads the `accessPolicy` BucketAccessClass parameter. The chart already
defines a `-readonly` BucketAccessClass carrying
`parameters.accessPolicy: readonly`, and the `bucket` app routes
`readonly: true` users to it — but `v0.1.2` ignores the parameter, so a
credential meant to be read-only is silently issued with full read-write
access and can upload and delete objects. This is an access-control bug.

Upstream `seaweedfs/seaweedfs-cosi-driver` added `accessPolicy` handling
in `v0.3.0`: `readonly` maps to `Read,List`, while `readwrite` and the
empty default stay `Read,Write,List,Tagging`. This bumps the driver to
`v0.3.1` via a `seaweedfs.cosi.image` override in the package
`values.yaml`, so the override survives `make update` and the vendored
chart is left untouched. The default read-write action set is unchanged,
so existing read-write credentials are unaffected; only read-only grants
are tightened to actually be read-only.

The `extra/seaweedfs` chart already runs `v0.3.1` with the same
objectstorage-sidecar, so the driver/sidecar combination is already
proven in this repo. A chart unit test pins the rendered driver image so
this fix cannot silently regress on a future re-vendor.

This does not close #1921. That issue bundles two features — read-only
bucket access (fixed here) and Object Lock / WORM (the `-lock`
BucketClass), which is independent and out of scope here. Relates to
#1921.

### Release note

```release-note
Fix SeaweedFS read-only bucket credentials being issued with read-write access; the COSI driver now honours the read-only access policy.
```


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

* **Bug Fixes**
* Pin the SeaweedFS COSI driver image to
`ghcr.io/seaweedfs/seaweedfs-cosi-driver:v0.3.1` to keep access behavior
consistent and avoid relying on a vendored default.
* Ensure the rendered SeaweedFS COSI deployment uses the expected
container name and the pinned driver image.
* **Tests**
* Add a YAML test suite to validate the generated COSI deployment
template matches the expected container name and pinned image.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 16:04:00 +03:00
myasnikovdaniil 3e20a0cd41 fix(kubernetes): make node group resources optional when instanceType is set (#3121)
## What this PR does

A tenant Kubernetes node group can be sized two ways: by `instanceType`
or by an explicit `resources` block. The generated values schema,
however, marked `resources` as required on every node group, so a node
group sized purely by `instanceType` was rejected at install time:

```text
at '/nodeGroups/<name>': missing property 'resources'
```

The same failure occurs through the typed `Kubernetes` CR: an empty
`resources: {}` is dropped during the CR-to-HelmRelease translation, so
by validation time the key is absent and the required check fails — even
though sizing was already fully specified by `instanceType`.

This marks `resources` optional in the node group definition and
regenerates the dependent artifacts (`values.schema.json`, API types,
README, and the `ApplicationDefinition`). No change is needed to emit
the VM: the chart already renders `domain.cpu`/`domain.memory` only when
`resources` is present and `instancetype` only when set, so a node group
sized purely by `instanceType` now validates and installs. Node groups
that specify `resources` are unaffected.

`instanceType` and `resources` are alternative, mutually exclusive
sizing sources — KubeVirt rejects a VirtualMachine that references an
instancetype and also sets explicit `domain.cpu`/`domain.memory`, since
instancetype CPU/memory cannot be overridden. To avoid emitting a VM
that the instancetype admission webhook would later reject, the chart
now fails fast at render time with a clear message when a node group
sets both a non-empty `instanceType` and explicit `resources`.

Tests: helm-unittest coverage proves a node group with `instanceType`
and no `resources` passes schema validation and renders without explicit
`domain.cpu`/`memory`; a complementary case confirms explicit
`resources` are still honoured; and a `failedTemplate` case pins the
render-time rejection of the conflicting both-set combination. The
omit-resources case fails against the pre-fix schema with the exact
error above, so it is a real regression guard.

Addresses #3046.

### Screenshots

N/A — schema and chart change, no UI impact.

### Release note

```release-note
fix(kubernetes): allow a node group to omit `resources` when it is sized by `instanceType` (previously failed schema validation with "missing property 'resources'"); reject the unsupported combination of `instanceType` and explicit `resources` at render time
```


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

* **New Features**
* Worker node groups can optionally size CPU/memory via `resources`, and
can omit `resources` when using `instanceType`.
* **Bug Fixes**
* Render now validates CPU/memory rules: `instanceType` with only one of
`resources.cpu`/`resources.memory` fails, and `instancetype` is omitted
when both `resources.cpu` and `resources.memory` are set (with
`instanceType` ignored).
* **Documentation**
* Updated API/Helm docs and JSON schema to clarify `resources` vs
`instanceType` precedence and the “set both CPU and memory (or neither)”
requirement.
* **Tests**
* Added helm-unittest scenarios covering omission, precedence, and
render-time failure cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 17:46:54 +05:00
Aleksei Sviridkin bf14a73b39 feat(api): add SecurityGroup network policy resource (sdn.cozystack.io) (#2922)
## What this PR does

Introduces a tenant-facing `SecurityGroup` resource in a new
`sdn.cozystack.io/v1alpha1` API group, served by the Cozystack
aggregated API server as a 1:1 projection of a `CiliumNetworkPolicy`. A
SecurityGroup attaches to one of the tenant's managed applications by
reference (`spec.targetRef: {apiGroup, kind, name}`) and declares
allow-list ingress/egress for that application. Tenants manage network
policy for their own apps entirely through
`securitygroups.sdn.cozystack.io`, without being granted any access to
the `cilium.io` API group.

The translation is synchronous and stateless — the API server converts
each SecurityGroup into a CiliumNetworkPolicy in the same namespace on
write and back on read; there is no controller. The backing policy's
`endpointSelector` is **derived** from `targetRef` (matched on the
application's lineage labels
`apps.cozystack.io/application.{group,kind,name}`), never copied from
tenant input, so the selector is machine-generated and can only ever
match the referenced application's own pods in the tenant's namespace.
Policies owned by this API carry an `sdn.cozystack.io/securitygroup`
marker label that the storage always re-asserts and hides from the view,
so it only ever surfaces or mutates its own policies and never touches
platform-managed network policies.

The value is self-service network policy on a Cozystack-native,
RBAC-scoped surface, without exposing Cilium's version-coupled CRDs to
tenants. Because the selector is derived from an authorized application
reference rather than free-form tenant input, the RBAC boundary is
structural, not selector-validated: a tenant cannot express network
policy against arbitrary or platform-owned pods. The full design —
selector derivation from lineage labels, the structural boundary, the
additive-policy reality (a SecurityGroup widens a managed application's
allowed traffic but cannot sever its platform-managed management plane,
and equally cannot tighten below the platform baseline), the in-tree
CiliumNetworkPolicy mirror that avoids a Cilium Go-module dependency,
RBAC, and deferred extensions (raw-pod targeting, cluster scope,
reusable groups) — is in `pkg/apis/sdn/DESIGN.md`.

Implements the attachment model from #1614.

### Release note

```release-note
feat(api): add the SecurityGroup resource (sdn.cozystack.io/v1alpha1) — a tenant-facing, namespace-scoped firewall that attaches to a managed application by reference and projects 1:1 onto a CiliumNetworkPolicy with a machine-derived endpoint selector, letting tenants manage their application's network policy without cilium.io access.
```

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

* **New Features**
* Added the `SecurityGroup` API resource (`sdn.cozystack.io` v1alpha1)
with a namespaced projection backed by Cilium network policies.
* Enabled the SDN `APIService` and wired SDN routing, scheme, and REST
support.
* Expanded RBAC to include SecurityGroup access and required Cilium
policy permissions.
* **Documentation**
* Added a design document describing projection behavior and rule
semantics.
* **Bug Fixes**
  * Improved API rule validation coverage for missing list-type fields.
* **Tests**
* Added SDN round-trip, fuzzing, RBAC, and comprehensive REST
create/get/list/update/delete/watch/resource-version tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 14:30:23 +03:00
Aleksei Sviridkin a349c753ad fix(kubernetes): make node group resources optional and prefer them over instanceType
A node group is sized either by instanceType or by explicit resources, but the
generated values schema required resources on every node group. A node group
with instanceType and no resources — or an empty resources dropped during
typed-CR to HelmRelease translation — failed schema validation with "missing
property 'resources'" even though sizing was fully specified.

Mark resources optional in the node group definition and regenerate the schema,
API types, README, and ApplicationDefinition. The chart template emits
domain.cpu/memory only when resources is present, so a node group sized purely
by instanceType now passes validation and installs.

When a node group sets both instanceType and explicit resources, explicit
resources take precedence: the instancetype is omitted from the rendered VM and
the per-group instancetype lookup is skipped, so KubeVirt accepts the explicit
domain.cpu/memory with no instancetype to conflict with. instanceType stays
required and defaulted (no schema change), so the console's required
instanceType dropdown keeps working — sending the default instanceType plus
explicit resources simply sizes the VM by resources. This replaces an earlier
render-time hard failure on the both-set case, which was unreachable to clear
from the dashboard (the required dropdown cannot be emptied).

Explicit resources are all-or-nothing alongside an instanceType: a partial spec
(only cpu, or only memory) on a group that carries a non-empty instanceType
would emit one domain override next to the instancetype — which KubeVirt rejects
— so it now fails at render time with a clear error. Partial resources on a
group without an instanceType (used to tune kubelet reservations) are unaffected.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-30 13:32:15 +03:00
Aleksei Sviridkin 0c6426feaa fix(seaweedfs): enforce read-only bucket access by bumping cosi-driver to v0.3.1
The seaweedfs COSI driver shipped at v0.1.2 hardcodes read-write S3
actions (Read, Write, List, Tagging) for every BucketAccess and never
reads the accessPolicy parameter. The chart already defines a -readonly
BucketAccessClass carrying parameters.accessPolicy: readonly, but v0.1.2
ignores it, so a credential meant to be read-only can still write
(upload and delete) objects.

Bump the driver to v0.3.1, which honours accessPolicy: readonly by
issuing Read,List-only credentials (read-write and the empty default
stay Read,Write,List,Tagging). The override lives in the package
values.yaml rather than the vendored chart so it survives make update;
the sister extra/seaweedfs chart already runs v0.3.1.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-30 13:19:39 +03:00
Aleksei Sviridkin 7c67aafed5 fix(kubernetes): run the Talos reconcile Job in the main install phase (#3145)
## What this PR does

The tenant worker `MachineDeployment` `md0` intermittently never reaches
`status.replicas=2`. CAPI blocks MachineSet creation until the
`TalosConfigTemplate` that the deployment's `bootstrap.configRef` points
to exists, and on failing installs that template is never created.

### Root cause — an ordering deadlock

The `TalosConfigTemplate` is created only by the `talos-reconcile`
**post-install Helm hook** (the chart can no longer render it at
template time — three of its inputs are produced asynchronously after
apply). A post-install hook runs only after Helm has applied the
release's main resources and, when Helm waits, after they become Ready.

But the same release also emits in-tenant addon HelmReleases — notably
**cilium** — whose readiness needs tenant worker nodes, and those nodes
can only be created once this hook has produced the
`TalosConfigTemplate`. So when the main-resource wait is in effect, the
install deadlocks: it waits for cilium to be Ready, cilium waits for
worker nodes, the nodes wait for the `TalosConfigTemplate`, and the
template waits for the post-install hook — which never runs, because the
install never finishes the wait that precedes the hook phase. The
install times out and `md0` never scales.

The release sets `helm-install-disable-wait` precisely to skip that
main-resource wait so the hooks run, but its effect is unreliable —
which is what makes the failure bimodal: when the wait is skipped the
hook runs and the template lands in ~46s; when it is not, the deadlock
strands the install.

A timestamped poller over a failing run (both `kubernetes-latest` and
`kubernetes-previous`, 640 polls) captured: `TalosConfigTemplate` absent
in 640/640 polls; both post-install hook Jobs (`talos-reconcile` and
`bootstrap-token`) never created — the install never reached the
post-install hook phase; all main resources applied (MachineDeployment,
KubevirtMachineTemplate, MachineHealthCheck, WorkloadMonitor, child
HelmReleases, Talos PKI/secrets) and the four hook inputs ready early
and stable; the parent HelmRelease stuck at `Running 'install' action
20m` then looping into `upgrade`, with the cilium sub-release timing out
on `cilium-operator` (which has no worker node to schedule on) —
downstream of the missing template.

### The fix

Render the `talos-reconcile` Job and its RBAC / ServiceAccount / egress
`CiliumNetworkPolicy` as ordinary **main-phase** resources instead of
post-install hooks. The Job is then created during the apply itself —
not after the wait — and runs on the management cluster, which has nodes
(unlike the tenant `cilium-operator`). It produces the
`TalosConfigTemplate`, `md0` scales, workers join, cilium goes Ready,
and the main-resource wait (if any) closes within the install budget:
the Job completes in tens of seconds, and node boot plus cilium
readiness is a few minutes — comfortably inside the 20m install timeout.
This works whether or not `helm-install-disable-wait` takes effect, so
it does not depend on that annotation's reliability.

Because a main-phase Job is immutable, it carries a content-hash name
suffix over its whole rendered spec (mirroring the
`KubevirtMachineTemplate` idiom in `cluster.yaml`): an identical render
keeps the same name and Helm no-ops it; any spec change yields a new
name and a fresh Job, so no change ever lands on the same name and trips
an immutable-field patch error. The applied `TalosConfigTemplate` keeps
its stable `<release>-<group>` name (referenced by
`bootstrap.configRef`), so an unchanged render re-applies a
byte-identical template — a CAPI no-op that never rolls existing
workers. The Job name is truncated to stay within the 63-char DNS-label
limit while always preserving the hash. The runtime input-waits, the
single `kubectl apply` of the template with a `KamajiControlPlane`
ownerReference for GC, and the `extraHostEntries` are all unchanged.

`helm-install-disable-wait`'s unreliable effectiveness is a real latent
issue (it explains the created-vs-never bimodality), but this fix breaks
the deadlock independent of it, so that is a separate follow-up rather
than a blocker here.

Supersedes #3139 (a 10-minute wait-budget bump that treated the symptom;
the deadlock is structural and a larger budget would not have helped).

### Screenshots

Not applicable — no UI changes.

### Testing

helm-unittest extended to pin: no `helm.sh/hook` on any of the seven
talos-reconcile documents, the content-hash name,
`ttlSecondsAfterFinished`, the preserved template apply + ownerReference
+ extraHostEntries, the hash rotating on a spec change, and the
long-name truncation staying within 63 chars. Full chart suite green
(128 tests). The applied template is byte-identical to the previous hook
output (verified by diff), so existing tenants are upgrade-safe.

### Release note

```release-note
fix(kubernetes): create the tenant worker TalosConfigTemplate from a main-phase Job instead of a post-install hook, so worker MachineDeployments reliably scale during install
```

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved Kubernetes addon install behavior by running reconcile work
during the main chart phase, reducing readiness deadlocks and upgrade
issues.
* Added more reliable reconciliation handling with stable,
change-detected job recreation and updated network access for the
reconcile workload.
* Refined chart and test guidance to match the new install flow and Helm
behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 12:29:55 +03:00
Aleksei Sviridkin f9d1138c6e feat(keycloak): configurable login theme via platform branding (#3124)
## What this PR does

Makes a realm's Keycloak login theme configurable through platform
`branding` values, closing a gap: `branding` could set the realm display
name but not its login theme, and the keycloak chart's `themes:` list
was not reachable from platform values.

Two new keys under `branding` (propagated via `_cluster.branding`, the
same path that already feeds `displayName`):

- `branding.loginTheme` — keycloak-configure renders it as
`ClusterKeycloakRealm.spec.themes.loginTheme`, so the realm selects that
login theme.
- `branding.loginThemeImage` — when set together with `loginTheme`, the
keycloak chart appends `{name: loginTheme, image: loginThemeImage}` to
its effective theme list, so the existing init-container mounts the
theme image into `/opt/keycloak/themes/`. No StatefulSet patching or
per-package values plumbing.

`loginTheme` alone (no image) still selects the theme — for themes
already baked into the Keycloak image. Explicit chart `themes:` entries
keep working; the branding-derived entry is appended after them.

Flow: platform `branding` → `_cluster.branding` (cozystack-values
Secret) → keycloak-configure (realm loginTheme) + keycloak (theme init
container). Covered by helm-unittest in both charts.

### Screenshots

No bundled UI assets — this PR adds the mechanism only; the login theme
is operator-supplied via `loginThemeImage`, so the visual result depends
on that image.

### Release note

```release-note
feat(keycloak): select the realm login theme and ship its theme image through platform `branding` values (`branding.loginTheme`, `branding.loginThemeImage`)
```


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

## Summary by CodeRabbit

* **New Features**
* Added support for configuring a custom login theme from platform
branding, including automatic selection in the realm setup.
* Theme entries can now be combined with existing theme settings and
rendered in the expected order.

* **Documentation**
* Expanded configuration guidance for branding and theme-related
settings with clearer examples.

* **Tests**
* Added coverage for login theme rendering and fallback behavior when
theme settings are missing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 12:29:52 +03:00
Aleksei Sviridkin 31c15b282f feat(network): mechanism-agnostic LoadBalancer exposure (ExposureClass + ServiceExposure) (#3081)
## What this PR does

Lays the foundation for migrating tenant LoadBalancer exposure off the
deprecated `Service.spec.externalIPs` (KEP-5707) and, eventually, off
MetalLB onto Cilium-native announcement — the mechanism-agnostic shape
prescribed when the earlier chart-side approach (#2468) was reverted in
#2603.

It adds a new `network.cozystack.io/v1alpha1` API group and a controller
that translates intent into whichever LoadBalancer backend the cluster
admin configured:

- `ExposureClass` (cluster-scoped, admin): binds a logical class name to
a backend (`externalIPs` | `metallb` | `cilium` | `robotlb`) and its VIP
address scope — the StorageClass/IngressClass pattern.
- `ServiceExposure` (namespaced, tenant intent): "expose this Service
via class X"; status reports the resolved backend and assigned IPs.
- A `serviceexposure` controller renders the backend's pool + announcer
once per class (MetalLB `IPAddressPool` + `L2Advertisement`; Cilium
`CiliumLoadBalancerIPPool` + `CiliumL2AnnouncementPolicy`), scoped to
the namespaces that opted in. Backends are pluggable behind a small Go
interface, so adding BGP or another cloud LB is one implementation, not
an API change. The controller never mutates the target Service — the
chart still owns its shape.

Platform wiring: `publishing.exposureClass.*` renders the admin
`ExposureClass` and the host ingress publishes through it. The default
(no class set, `externalIPs`) leaves every existing cluster's behaviour
byte-for-byte unchanged; the MetalLB→Cilium announcer migration becomes
a single `ExposureClass.spec.backend` change.

This is the foundation only — Part of #2586. Converting per-app
exposure, flipping Cilium `l2announcements` on, changing the default,
and removing MetalLB are deferred to later phases.

### Release note

```release-note
feat(network): add ExposureClass and ServiceExposure for mechanism-agnostic LoadBalancer exposure. Opt-in foundation; the default externalIPs publishing path is unchanged.
```


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

* **New Features**
* Added new `ExposureClass` and `ServiceExposure` custom resources with
support for multiple backends (externalIPs, MetalLB, Cilium, RobotLB),
including optional L2 and address configuration.
* Added Helm wiring and templates to enable service/ingress publishing
via `ServiceExposure`, selecting a configured (or default)
`ExposureClass`.
* **Bug Fixes**
* Improved reconciliation, finalizer-based cleanup, and status updates
for readiness and assigned IP reporting, including safer handling when
backends or managed resources change.
* **Tests**
* Added end-to-end and unit test coverage for backend rendering,
readiness, and cleanup flows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 12:29:48 +03:00
mattia-eleuteri 7e3c5f99db fix(kubernetes): apply default md0 node group only when nodeGroups is empty
The chart shipped a default nodeGroups.md0 in values.yaml. Helm deep-merges
values, so the default md0 was re-added on top of any user-supplied
nodeGroups; combined with Kubernetes stripping null values, md0 could never
be removed by configuration. Move the default into the template
("kubernetes.nodeGroups" helper) so it is applied only when nodeGroups is
empty. User-supplied node groups now fully replace the default, letting users
name and omit groups (including md0) freely.

Test fixtures that relied on the chart-default md0 to supply diskSize via
values merge now set it explicitly.

Signed-off-by: mattia-eleuteri <mattia@hidora.io>
2026-06-30 12:08:17 +05:00
Aleksei Sviridkin 32c4d60b43 fix(kubernetes): run the Talos reconcile Job in the main install phase
The talos-reconcile Job is the only thing that creates the
TalosConfigTemplate the worker MachineDeployment's bootstrap.configRef
points to; CAPI blocks MachineSet creation until it exists, so tenant
workers cannot scale until the Job has run.

The Job was a post-install/post-upgrade Helm hook, which runs only after
Helm has applied the release's main resources and, when Helm waits, after
they become Ready. But the same release emits in-tenant addon HelmReleases
— notably cilium — whose readiness needs worker nodes, and those nodes
need the TalosConfigTemplate this Job produces. So when the main-resource
wait is in effect the install deadlocks: it waits for cilium to be Ready,
cilium waits for workers, the workers wait for the template, and the
template waits for this hook — which never runs because the install never
finishes the wait that precedes the hook phase. The install times out and
the MachineDeployment never scales. The release sets
helm-install-disable-wait to skip that wait, but its effect is unreliable,
which makes the failure bimodal: the template lands in ~46s when the wait
is skipped, and the install is stranded when it is not.

Render the Job and its RBAC/ServiceAccount/egress policy as ordinary
main-phase resources. The Job is then created during the apply itself, not
after the wait, and runs on the management cluster where nodes exist
(unlike the tenant cilium-operator), so it produces the template, the
MachineDeployment scales, workers join, cilium goes Ready, and the wait
(if any) closes within the install budget — independent of whether
helm-install-disable-wait takes effect.

The Job is immutable, so it carries a content-hash name suffix over its
rendered spec (mirrors the KubevirtMachineTemplate idiom in cluster.yaml):
an identical render keeps the name and Helm no-ops it, while any spec
change yields a new name and a fresh Job, so no spec change ever lands on
the same name and triggers an immutable-field patch error. The applied
TalosConfigTemplate keeps its stable <release>-<group> name, so an
unchanged render re-applies a byte-identical template — a CAPI no-op that
never rolls existing workers.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-30 07:01:51 +03:00
Aleksei Sviridkin 6896a2a4db feat(kubernetes): expose tenant apiserver authentication in app values (#3123)
## What this PR does

The managed `Kubernetes` app (Kamaji-backed tenant control planes)
exposed no way to configure the tenant `kube-apiserver` authentication
through the application values — there was no knob for OIDC, the
structured `AuthenticationConfiguration`, extra apiserver flags, or
extra volumes. The only workaround was patching the underlying
`KamajiControlPlane` out-of-band, which is invisible to GitOps and lost
when the cluster is recreated from values.

This adds three opt-in fields under `controlPlane.apiServer`, all empty
by default so existing clusters render unchanged:

- `extraArgs` → `KamajiControlPlane.spec.apiServer.extraArgs`
- `extraVolumeMounts` → `spec.apiServer.extraVolumeMounts`
- `extraVolumes` → appended to `spec.deployment.extraVolumes`

This unblocks OIDC SSO via the `--oidc-*` flags and, more importantly,
structured authentication: `--authentication-config` mounted from a
ConfigMap/Secret plus `--requestheader-uid-headers`, which is the only
way to map a user UID for aggregated/extension apiservers.

Because the control-plane Deployment runs on the management cluster, the
passthrough is validated to keep tenant and management isolated:
`extraVolumes` accept only `configMap`/`secret` sources (host-reaching
sources such as `hostPath`/`csi` and token projection via `projected`
are rejected); each volume needs a unique, non-empty name and exactly
one source; the reserved `talos-ca`/`talos-tls-cert` names are rejected;
and each `extraVolumeMount` must reference a user-declared
`extraVolume`, so the chart-managed talos secret volumes cannot be
mounted. Secret/ConfigMap references resolve in the tenant's own
management-side namespace and are intentionally allowed — this stays
within the tenant's existing security domain.

Generated artifacts (`values.schema.json`, README, Go types + deepcopy,
ApplicationDefinition) are regenerated from the values annotations. Helm
unit tests cover the default no-op render, each field's passthrough, and
every rejection path.

Addresses #3074.

### Screenshots

N/A — no UI changes.

### Release note

```release-note
feat(kubernetes): expose tenant apiserver authentication via controlPlane.apiServer.extraArgs / extraVolumes / extraVolumeMounts, enabling OIDC and structured AuthenticationConfiguration on tenant clusters
```


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

## Summary by CodeRabbit

* **New Features**
* Added support for customizing the Kubernetes API server with extra
command-line arguments, additional volume mounts, and extra volumes.
* Updated chart values and schema so these options can be configured
directly.

* **Bug Fixes**
* Added validation to prevent invalid or conflicting volume names and
disallowed volume sources.
* Ensured mounts only reference declared extra volumes and reserved
control-plane names can’t be overridden.

* **Tests**
* Added coverage for default behavior, valid customization paths, and
invalid configuration scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-29 13:13:54 +03:00
myasnikovdaniil 80e3fa9cbe fix(dashboard): render error and unknown-type states instead of an infinite spinner (#3098)
## What this PR does

Two console pages ordered their loading guard ahead of the terminal
state, making the latter unreachable. Both are pre-existing upstream
behaviors carried in by the cozystack-ui vendoring (#2963) and flagged
as non-blocking findings in that PR's review.

- **`ApplicationDetailPage`** checked `isLoading || !instance || !ad`
before `if (error)`. On a failed GET (403/404/5xx) `isLoading` is false
and `instance` stays undefined, so the page rendered the loading spinner
forever and never reached the "not found" branch. The error branch now
runs first.
- **`ApplicationListPage`**'s `defsLoading || (!ad && plural)` guard
stayed true for any unrecognized `plural` once definitions had loaded,
so the "Unknown application type." branch was unreachable. The guard now
only spins while definitions are loading.

## Verification

- Added co-located regression tests for the failed-GET (detail) and
unknown-plural (list) paths.
- `pnpm typecheck` clean; full console suite green (`pnpm test`: 317
passed, +4 new).

```release-note
fix(dashboard): the console now shows a not-found message on a failed application GET and an unknown-type message for an unrecognized resource, instead of spinning forever
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved loading and error handling on application pages so users see
the correct message at the right time.
* “Unknown application type” now appears only after application
definitions finish loading, instead of being masked by a loading state.
* Application detail pages now show a “not found” message when the
requested app can’t be loaded, rather than continuing to display a
spinner.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-29 14:02:49 +05:00
myasnikovdaniil 61a574393a build(dashboard): pin console image builder to pnpm 11.1.2 and repoint vendor doc link (#3099)
## What this PR does

Two post-vendor cleanups from the #2963 review:

- **Pin the console image builder to pnpm 11.1.2.** The `Containerfile`
ran `corepack prepare pnpm@latest`, so the container build could drift
to a newer pnpm major than the `pnpm@11.1.2` pinned in `package.json`
and used by CI. `--frozen-lockfile` guards the lockfile, but pinning the
builder makes the image build reproducible and consistent with CI.
- **Repoint a stale doc link.** `docs/storage-immutability.md`
referenced the now-vendored `cozystack/cozystack-ui#6`; it now points at
the in-tree console path.

## Verification

- No source change; `pnpm typecheck` + `pnpm test` unaffected.
- Builder pnpm version now matches `package.json`'s `packageManager`
field.

```release-note
NONE
```


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

## Summary by CodeRabbit

* **Documentation**
* Clarified the storage immutability documentation by updating the UI
reference to an in-repo path.

* **Chores**
* Standardized the build environment to use a specific pnpm version,
helping keep installs and builds consistent.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-29 14:02:28 +05:00
Aleksei Sviridkin e78a5842f5 feat(keycloak): ship a login theme image via branding
Build the StatefulSet theme list from .Values.themes plus, when
branding.loginTheme and branding.loginThemeImage are both set, an auto-derived
entry. This lets an operator deliver a Keycloak login theme through platform
branding values alone (the init container mounts the theme image) without
patching the StatefulSet or wiring per-package values. branding arrives via
_cluster.branding, the same path that already feeds the realm displayName.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-28 05:08:38 +03:00
Aleksei Sviridkin fd9100c632 feat(keycloak-configure): select realm login theme from branding
Render ClusterKeycloakRealm.spec.themes.loginTheme from
_cluster.branding.loginTheme, alongside the existing brandName -> displayName
path. Without it a realm keeps the stock Keycloak login theme and there is no
declarative way to switch it through platform values.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-28 05:08:27 +03:00
Aleksei Sviridkin 243ce65071 feat(kubernetes): expose tenant apiserver authentication in app values
Tenant Kubernetes clusters had no way to configure the kube-apiserver
authentication through the app values: no knob for OIDC, the structured
AuthenticationConfiguration, extra apiserver flags, or extra volumes.
The only workaround was patching the underlying KamajiControlPlane
out-of-band, which is invisible to GitOps and lost on recreation.

Add three opt-in fields under controlPlane.apiServer, all empty by
default so existing clusters render unchanged:

- extraArgs        -> KamajiControlPlane spec.apiServer.extraArgs
- extraVolumeMounts -> spec.apiServer.extraVolumeMounts
- extraVolumes     -> appended to spec.deployment.extraVolumes

This unblocks OIDC SSO via the --oidc-* flags and, more importantly,
structured authentication: --authentication-config mounted from a
ConfigMap/Secret plus --requestheader-uid-headers, which is the only
way to map a user UID for aggregated/extension apiservers.

Because the control-plane Deployment runs on the management cluster,
the passthrough is validated to preserve tenant<->management isolation:

- extraVolumes allow only configMap and secret sources; host-reaching
  sources (hostPath, csi, nfs, persistentVolumeClaim) and token
  projection (projected) are rejected, so a tenant with self-service
  access to the Kubernetes CR cannot mount management-node paths or
  project arbitrary tokens into the apiserver pod.
- each extraVolume must have a unique, non-empty name and exactly one
  source; the reserved names talos-ca and talos-tls-cert are rejected
  to avoid a duplicate volume name in the rendered podspec.
- each extraVolumeMount must reference a user-declared extraVolume; the
  chart-managed talos secret volumes cannot be mounted.

Regenerate values.schema.json, README, the Go types and deepcopy, and
the ApplicationDefinition. Add helm unittest coverage for the default
(no-op) render, each field's passthrough, the name/source guards, and
the mount-reference guard (valid and rejected inputs).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-28 04:01:39 +03:00
Aleksei Sviridkin 8a14de9abf fix(seaweedfs): point S3 consumers at the reachable S3 service
The s3-service-name.patch renamed the S3 Service to <name>-s3
(seaweedfs-s3), but several consumers still resolved it through
componentName -> <fullname>-s3. name and fullname only coincide when the
release name equals the chart name; the shipped release is
seaweedfs-system, so they pointed at seaweedfs-system-s3 while the real
service stayed seaweedfs-s3:

- the S3 ingress backend and the iceberg ingress backend, which left
  ingress-nginx returning 503 on the external S3 endpoint.

The COSI provisioner in-cluster ENDPOINT was wrong on the same axis and
also on the port. Both the s3-gateway branch and the filer-S3 branch
rendered a portless URL that defaults to 443, while the s3 service only
listens on the S3 port (8333); the filer branch additionally targeted
the filer service, which serves no S3. Both in-cluster branches now
resolve seaweedfs-s3 on the configured S3 port.

The default path (s3.ingress.enabled=true) is repaired by the ingress
fix; the COSI in-cluster paths are latent until an operator disables the
ingress. Resolve every consumer through the same name helper the Service
uses, and pin the contract with unittests rendered under the production
release name so the mismatch cannot recur.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-27 22:26:40 +03:00
Myasnikov Daniil 6423cb53e0 fix(kubernetes): never auto-select the kubevirt alias as tenant default SC
The legacy "kubevirt" StorageClass alias was inserted into the candidate
map before the default was chosen, so the sortAlpha fallback could pick it
when propagated class names sort after "kubevirt" (e.g. nvme, ssd) and no
configured/replicated class is present. That made the tenant default point
at an infraStorageClass that may not exist on the infra cluster, leaving
default PVCs Pending. Choose the default from the propagated set before
adding the alias.

Also quote the templated StorageClass name and infraStorageClassName in
kubevirt-csi-node, and document the propagation upgrade caveat, render-time
evaluation limitation, and allowRemoteVolumeAccess assumption in the
kubernetes README.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-06-26 21:20:30 +05:00
Myasnikov DaniilandClaude Opus 4.8 6a1b56f2b5 test(kubernetes): cover LINSTOR StorageClass propagation
- csi_test.yaml: assert the fallback storageClasses map yields a default
  "replicated" plus the non-default "kubevirt" alias, and that the
  alias tracks the storageClass selector.
- kubevirt-csi-node: add a Makefile test target (otherwise the package
  is silently skipped by the unit-test runner) and storageclasses_test
  covering per-entry rendering, the single-default invariant, and the
  absence of any enforcement allowList in driver-config.
- run-kubernetes.sh: verify in e2e that "replicated" propagates as the
  default kubevirt-CSI class, the "kubevirt" alias is retained, and the
  node-local "local" infra class is filtered out.

Refs: #2094

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-06-26 21:20:30 +05:00
Myasnikov DaniilandClaude Opus 4.8 021f6141ca feat(kubernetes): propagate remote-accessible LINSTOR StorageClasses to tenant clusters
Tenant Kubernetes clusters previously exposed a single hardcoded
"kubevirt" StorageClass mapped to one infra class. Discover all
remote-accessible LINSTOR infra StorageClasses (provisioner
linstor.csi.linbit.com, allowRemoteVolumeAccess != false) via Helm
lookup and propagate each to the tenant under the same name.

- csi.yaml builds a storageClasses map filtered to remote LINSTOR
  classes; node-local classes (e.g. "local") are excluded because the
  backing volume cannot follow a live-migrated worker VM.
- The class named by .Values.storageClass (default "replicated") is
  marked the tenant default; the legacy "kubevirt" StorageClass is kept
  as a non-default alias so existing tenant PVCs keep binding.
- A non-destructive fallback preserves the legacy mapping when the
  lookup returns empty (dry-run), so tenant StorageClasses are never
  wiped on a transient reconcile.
- infraStorageClassEnforcement is intentionally left disabled (AllowAll)
  to avoid breaking tenants that already self-served custom
  StorageClasses under the current default.

Refs: #2094, #1424

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-06-26 21:20:29 +05:00
myasnikovdaniil 1df8c7c557 feat(kubernetes): bootstrap tenant workers with Talos via CABPT (#2931)
## What this PR does

> **Supersedes #2610.** Same work, rebased cleanly onto current `main`
so it now sits on top of #2867 (the `kube-controller-manager` VAP
type-checker fix). That fix is what was killing the base install in
#2610's E2E *before* the `kubernetes` app test ever ran, leaving the
Talos worker-bootstrap rewrite verified only by code reading. #2610's
67-commit branch is squashed here into 8 logical commits with original
authorship preserved (@kvaps and @IvanHunters as authors); the tree is
**byte-identical** to #2610's head — no code changes, just the rebase
onto `main` (to pick up #2867) plus history cleanup. The detailed review
history lives on #2610.

Phase 1 of the Kubernetes-app split design (see cozystack/community#8):
replace the Ubuntu+kubeadm worker bootstrap path with Talos, driven by
`cluster-api-bootstrap-provider-talos` (CABPT) and a
`clastix/talos-csr-signer` sidecar embedded in the Kamaji control-plane
pod. Existing tenants keep working — old machines roll out and get
replaced by Talos workers without manual intervention.

Highlights:

- Add CABPT (v0.6.12) as a second `BootstrapProvider` alongside the
existing kubeadm one in `capi-providers-bootstrap`.
- Bump the Kamaji control-plane provider with an upstream-bound patch
that exposes `KamajiControlPlane.spec.network.additionalServicePorts`,
used to publish trustd (50001/TCP) on the apiserver service.
- Generate Talos PKI (Ed25519 CA + trustd TLS) via cert-manager and
stable random Talos secrets (`token`, `clusterId`, `clusterSecret`,
`bootstrapToken`) using the helm lookup-and-reuse pattern.
- Materialise a kubeadm-format `bootstrap-token-<id>` Secret inside the
tenant `kube-system` via a Helm post-install/upgrade Job.
- Render a `TalosConfigTemplate` (worker machineconfig, `generateType:
none`) and switch the `MachineDeployment.bootstrap` reference from
`KubeadmConfigTemplate` to `TalosConfigTemplate`, gated on Talos secrets
and the Kamaji apiserver service being ready.
- Boot workers from the Talos openstack image via a CDI `DataVolume`
(`source.http.url` → `factory.talos.dev`), expose the system disk as
virtio-blk with `blockSize.custom: logical=512, physical=4096` so
4Ki-native block storage backends (LINSTOR/DRBD) play nicely with QEMU's
O_DIRECT writes and SeaBIOS still boots, drop the separate kubelet disk
(Talos lays out EPHEMERAL itself).
- Pin pod/service CIDRs in the worker machineconfig to the tenant ranges
(`10.243.0.0/16` / `10.95.0.0/16`) to avoid Talos's address-overlap
diagnostic against the host pod CIDR.
- Set `cluster.controlPlane.endpoint` and `cilium.k8sServiceHost` to the
Kamaji apiserver Service ClusterIP (looked up at render time), so
bootstrap survives the chicken-and-egg moment where tenant DNS does not
exist yet and the host CoreDNS does not serve the tenant zone.

End-to-end on a dev cluster: a fresh tenant brings up a Talos worker,
kubelet CSRs get approved (apiserver-client by Kamaji, kubelet-serving
by the talos-csr-signer sidecar), Cilium initialises with hostNetwork
against the apiserver ClusterIP, CoreDNS comes up, and the node
transitions to `Ready`.

### Release note

```release-note
feat(kubernetes): bootstrap tenant workers with Talos Linux instead of Ubuntu+kubeadm. Existing clusters roll over to Talos workers automatically; new tenants come up on Talos from the start. Powered by cluster-api-bootstrap-provider-talos and the talos-csr-signer sidecar in the Kamaji control-plane pod. Note: the tenant `kubernetes` HelmRelease now reports Ready as soon as Helm completes (Install/Upgrade DisableWait) and no longer blocks on worker/addon readiness — worker-rollout health is tracked by WorkloadMonitor, not the HelmRelease.
```


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

## Release Notes

**New Features**
* Added Talos worker configuration for `talos.version` and
`talos.schematicID`
* Introduced `nodeHealthCheck` (`maxUnhealthy`, `nodeStartupTimeout`)
for worker remediation tuning
* Added image overrides for `kubectl` and `talosCsrSigner`
* Exposed additional tenant control-plane Service ports support

**Breaking Changes**
* Removed Kubernetes v1.30; supported versions start at v1.31
* Renamed `nodeGroups.ephemeralStorage` → `nodeGroups.diskSize` with
consolidated Talos system-disk semantics

**Removals**
* Removed Ubuntu container-disk images and related containerd patch
mechanisms
* Air-gapped tenant workers temporarily unsupported during the Talos
transition

**Documentation**
* Updated chart docs, parameters, and GitOps upgrade guidance for
version bumps
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-26 20:06:49 +05:00
Myasnikov Daniil 133303ec12 build(dashboard): pin console image builder to pnpm 11.1.2 and repoint vendor doc link
The console Containerfile ran `corepack prepare pnpm@latest`, so the image build could drift to a newer pnpm major than the `pnpm@11.1.2` pinned in package.json and used by CI. `--frozen-lockfile` guards the lockfile, but pinning the builder makes the image build reproducible and consistent with CI.

Also repoints docs/storage-immutability.md off the now-vendored cozystack-ui#6 link to the in-tree console path (follow-up from #2963).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-06-26 20:00:12 +05:00
Myasnikov Daniil 4838bafbc1 fix(dashboard): render error and unknown-type states instead of an infinite spinner
Two console pages ordered their loading guard ahead of the terminal state, making the latter unreachable:

- ApplicationDetailPage checked `isLoading || !instance || !ad` before `if (error)`. On a failed GET, isLoading is false and instance stays undefined, so the page spun forever and never showed the not-found message. The error branch now runs first.

- ApplicationListPage's `defsLoading || (!ad && plural)` guard stayed true for any unrecognized plural once definitions had loaded, so 'Unknown application type.' was unreachable. The guard now only spins while definitions are loading.

Both are pre-existing upstream behaviors carried in by the cozystack-ui vendoring (#2963). Adds co-located regression tests for the failed-GET and unknown-plural paths.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-06-26 19:59:04 +05:00
myasnikovdaniil 01addfc267 build(dashboard): vendor cozystack-ui into the monorepo (#2963)
## What this PR does

Moves the `cozystack-ui` console SPA into this monorepo so the UI
versions and ships in lockstep with the platform — removing the
cross-repo digest-bump PRs and the version drift between the two
repositories.

The source is vendored at `packages/system/dashboard/images/console/`
via `git subtree`, preserving full upstream history. This mirrors the
existing in-tree `images/token-proxy/` pattern, and `.helmignore`
already excludes `images/*/*` so the source never enters the Helm chart.

**Changes**

- **Source**: `cozystack-ui` `main` merged under
`packages/system/dashboard/images/console/` (history preserved as a
merge parent).
- **Build**: `packages/system/dashboard/Makefile` builds the console
image from the in-tree context instead of cloning the external repo at
build time. Dropped `clone-console`/`clean-console` and the
`CONSOLE_REPO`/`CONSOLE_BRANCH` variables. The built digest is still
pinned into `values.yaml` by `update-values-console` — now in the same
commit as the source change rather than a separate follow-up PR.
- **CI**: new `.github/workflows/ui-test.yaml` runs pnpm typecheck +
unit tests, path-gated to `images/console/**`. The container image
continues to build via `make build`.

**Verification**

- `pnpm install --frozen-lockfile` → `pnpm typecheck` clean → production
build produces `dist/`.
- 313 unit tests pass.
- `helm template` renders; console + token-proxy image references
unchanged.

**Follow-ups (after merge)**

- Archive the standalone `cozystack-ui` repository and redirect
contributions here.
- Update remaining documentation pointers that reference the old repo
path.

### Screenshots

N/A — no UI behavior change. This relocates the UI source and rewires
the build; the shipped image and digest are unchanged.

### Release note

```release-note
build(dashboard): vendor the cozystack-ui console into the monorepo and build it from in-tree source; the shipped image and digest are unchanged for cluster operators
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-26 18:39:25 +05:00
myasnikovdaniil a83612bcb5 fix(vm-default-images): refresh stale Alpine and openSUSE 16.0 image URLs (#3063)
## What this PR does

Refreshes two default golden-image download URLs in `vm-default-images`
that had drifted from their canonical source. This came out of a
correctness re-check of all 16 default image URLs — the other 14 use
floating `current`/`latest` pointers on canonical vendor hosts and need
no change.

- **alpine-3.21** — was pinned to patch `3.21.6`. Alpine's CDN now ships
`3.21.7` and prunes older patches over time, so this hardcoded pin would
eventually 404. Bumped to `3.21.7`. (Alpine publishes no `latest`
symlink for cloud images, so a patch pin is unavoidable — it just needs
to track the newest patch.)
- **opensuse-leap-16.0** — pointed at the Open Build Service staging
tree (`repositories/openSUSE:/Leap:/16.0:/Images/`) rather than the
canonical released-product tree. Switched to
`distribution/leap/16.0/appliances/`, which serves the identical image
from a more stable, official location.

Both new URLs were verified to return HTTP 200 and serve valid QCOW2
images (magic `QFI\xfb`).

> Note: `opensuse-leap-15.6` sits on the same OBS staging path, but
moving it to the distribution tree would also require switching the
image flavor (NoCloud → Minimal-VM Cloud, the only variant published
there). That's a behavioral change, so it's intentionally left out of
this PR.

### Release note

```release-note
fix(vm-default-images): refresh stale Alpine (3.21.6 → 3.21.7) and openSUSE Leap 16.0 default image URLs
```


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

## Summary by CodeRabbit

* **Updates**
* Refreshed two default virtual machine image entries to point to newer
download sources.
* Updated the openSUSE Leap 16.0 image URL to a new appliance location.
  * Bumped the Alpine 3.21 image to a newer release version.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-26 17:52:07 +05:00
Kirill Ilin 5cb6ef435f feat(keycloak): opt-in separate hostname and route for the admin console (#2954)
## What this PR does

Adds an opt-in way to serve the Keycloak admin console and
Administration
REST API on a separate hostname and route from the public login
endpoints,
so the admin surface can be kept off the public internet.

- New `ingress.adminHost` in the keycloak chart. When set:
  - `KC_HOSTNAME_ADMIN` is configured so the admin console generates its
    URLs on that hostname;
  - the public (login) route is narrowed to the OIDC paths only
    (`/realms`, `/resources`, `/.well-known`), so the admin console and
    Administration REST API are no longer served on the login hostname;
- a dedicated route (Ingress or HTTPRoute, depending on
`gateway-enabled`)
    serves the admin hostname.
- New platform `publishing.ingressNameAdmin`, propagated as
  `_cluster.expose-ingress-admin`. The admin route attaches to the
Gateway/ingressClass it names, defaulting to `expose-ingress`. Pointing
it
at a separate (for example, private-IP) Gateway moves admin traffic off
the public one. The separate Gateway itself is provisioned out of band;
  this only selects where the admin route attaches.

Everything is empty by default, so existing deployments are unchanged.

Note: `KC_HOSTNAME_ADMIN` on its own is not a security boundary — the
Administration REST API stays reachable on any hostname routed to
Keycloak
(per the Keycloak docs). The path and network separation introduced here
is
what restricts access; full isolation requires attaching the admin route
to
a non-public Gateway.

Covered by helm-unittest suites for the keycloak chart and the platform
value wiring.

### Screenshots

N/A — no UI changes.

### Release note

```release-note
feat(keycloak): add opt-in `ingress.adminHost` to serve the admin console and Administration REST API on a separate hostname/route, plus `publishing.ingressNameAdmin` (`_cluster.expose-ingress-admin`) to attach that route to a separate (e.g. private) Gateway/ingressClass. Disabled by default; existing deployments are unchanged.
```


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

* **New Features**
* Added optional configuration to serve the Keycloak admin console on a
dedicated hostname, with separate routing and ingress/gateway attachment
so admin UI/API can be isolated from public login/OIDC paths.
* Introduced a cluster-level setting to optionally expose admin routes
via a separate ingress/gateway class/namespace.

* **Tests**
* Added new YAML test coverage to validate defaulting, hostname split
behavior, and correct routing for both gateway-enabled and
gateway-disabled modes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-26 14:08:23 +05:00
Aleksei Sviridkin 664704d7b6 feat(controller): reconcile ServiceExposure into backend LB resources
Add the serviceexposure controller to the cozystack-controller binary. It
resolves a ServiceExposure's ExposureClass to a backend and renders the
backend-specific pool and announcer CRs, then reports the assigned IPs in
status.

Backends are pluggable behind a small interface, one per file:

- externalIPs: renders nothing (the node owns the address) — preserves the
  historical default behaviour.
- metallb: IPAddressPool + L2Advertisement in cozy-metallb, scoped to the
  target Service's namespace.
- cilium: CiliumLoadBalancerIPPool (cilium.io/v2) + CiliumL2AnnouncementPolicy
  (cilium.io/v2alpha1), scoped to the Service via serviceSelector.
- robotlb: renders nothing (the cloud LB owns allocation and announcement).

The rendered CRs belong to foreign API groups not in the Go scheme, so they
are emitted as unstructured objects. They carry no OwnerReference (the
Cilium CRs are cluster-scoped and the MetalLB CRs live in a different
namespace than the owning ServiceExposure), so ownership is tracked via a
management label plus owner annotations, cleanup is driven by a finalizer,
and the controller refuses to take over an identically-named resource it
does not manage. The controller never mutates the target Service, leaving
its shape to the chart that renders it.

Grant the controller write access to metallb.io and cilium.io pool /
announcer resources.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-25 20:47:19 +03:00
IvanHunters 2cb1b4c665 fix(seaweedfs): raise master probe timeouts above the cluster-status backoff (#3068)
## What this PR does

The SeaweedFS master's `/cluster/status` HTTP handler calls
`topo.Leader()`, which backs off exponentially up to
`MaxElapsedTime=20s` while the node has no leader in view. A 3-master
cluster bootstrap is a race: a master that briefly becomes the initial
raft leader and is then demoted can sit with an empty leader-view for a
while, so its `/cluster/status` takes up to ~20s to return (it does
return 200 — just slowly). The master liveness and readiness probes both
`httpGet /cluster/status:9333` with `timeoutSeconds: 10`, which is
shorter than that worst case. Such a master is liveness-killed roughly
every ~137s into a permanent CrashLoop (the master uses an `emptyDir`
data dir, so each container restart re-reads the same raft state and
re-wedges), and its readiness never succeeds — so the master StatefulSet
never reaches full readiness and the `seaweedfs-system` HelmRelease
install times out.

This raises both master probe `timeoutSeconds` to 30 (above the 20s
backoff) via the overlay values, deep-merging onto the subchart defaults
so only the timeout changes. Both probes must be raised: leaving
readiness at 10s would keep the master NotReady on the same slow
endpoint even if liveness no longer kills it. The volume component in
this same chart already uses a 30s probe timeout, so this aligns master
with that precedent. This is purely probe tuning — no raft, protocol, or
storage change.

The underlying upstream behavior (`/cluster/status` slow for a master
with an unhealthy raft view) is tracked upstream and is unchanged across
current SeaweedFS releases, so a version bump does not address it.
Switching the master to the Hashicorp raft backend is a separate, larger
change (it pulls in bootstrap and storage considerations) and is
intentionally out of scope here.

Operational note: on chart update each master StatefulSet rolls one pod
at a time with the new timeouts (raft quorum is preserved); a platform
upgrade therefore performs a short graceful roll of master pods on each
instance — not a data outage.

Adds a helm-unittest pinning both master probe timeouts and guarding
that the subchart's other probe fields survive the deep-merge.

### Release note

```release-note
fix(seaweedfs): raise the master liveness/readiness probe timeouts above the cluster-status leader-lookup backoff so a master with a transiently empty raft view is not killed into a permanent CrashLoop, which could time out the seaweedfs install
```
2026-06-25 19:22:51 +03:00
IvanHunters dffe259d95 fix(kube-ovn): raise ovs-ovn CPU limit/request to relieve ovn-controller throttling (#3050)
## What this PR does

**Problem.** `ovn-controller` runs inside the `ovs-ovn` DaemonSet's
single `openvswitch` container, sharing one CPU limit with
`ovsdb-server` and `ovs-vswitchd`. The container CPU limit was `2` cores
and the request `10m`. For three latency-sensitive daemons on a busy
cluster that ceiling is tight: under a burst of pod creation (a large
install with many concurrent HelmReleases) `ovn-controller` is CPU-bound
while programming OpenFlow for newly created logical ports, and a `10m`
request leaves it almost no guaranteed share under node CPU contention.

**Change.** Raise the `ovs-ovn` container CPU limit from `2` to `4`
cores and the request from `10m` to `100m`, so the co-located daemons
have headroom under burst and `ovn-controller` keeps a guaranteed
scheduling share under contention. Memory is unchanged — the change is
scoped to CPU.

**Scope — hardening, not a proven fix.** This change started from a
hypothesis that CPU throttling was lengthening flow-programming and
causing transient host→pod unreachability (`connection refused` from a
pod's own node) during burst installs. A live trace of a captured
occurrence of that symptom showed **zero** `ovs-ovn` CPU throttling in
the failure window, so the throttle ceiling was **not** the cause of
that failure. This PR therefore lands as a cheap, safe, well-bounded
headroom improvement on a ceiling that is genuinely tight for three
co-located daemons — not as a fix for the unreachability flake, whose
root cause is separate and still open.

**Test.** `helm template` confirms the rendered `ovs-ovn` DaemonSet now
sets `requests.cpu: 100m` and `limits.cpu: 4` (with `ephemeral-storage:
1Gi` inherited from the subchart default). A new helm-unittest
regression guard pins the raised limit and request.

### Screenshots

No UI changes.

### Release note

```release-note
fix(kube-ovn): raise the ovs-ovn container CPU limit to 4 cores (request to 100m) so the co-located ovn-controller/ovsdb-server/ovs-vswitchd daemons have CPU headroom under burst installs
```
2026-06-25 19:22:17 +03:00
IvanHunters 857c883695 fix(monitoring-agents): relax fluent-bit probes for loaded installs (#3045)
## What this PR does

**Problem**

On a loaded install the `monitoring-agents` fluent-bit DaemonSet
crash-loops and its HelmRelease times out (`timeout waiting for:
[DaemonSet/.../monitoring-agents-fluent-bit status: 'InProgress']`),
cascading into velero → backupstrategy-controller `DependencyNotReady`
and failing the whole install.

Root cause: fluent-bit's HTTP server on `:2020` (the liveness/readiness
target) binds only AFTER the `filter:kubernetes` "testing connectivity
with API server" step. During a concurrent install the kube-API can be
unreachable for ~10s (observed `connection #144 timeout after 10 seconds
to: kubernetes.default.svc:443`), which delays the `:2020` bind. The
vendored subchart's default probes (`initialDelaySeconds 0,
timeoutSeconds 1, failureThreshold 3`) fire into a connection-refused
port immediately, and the single-threaded HTTP server can't always meet
a 1s timeout while the engine is busy — so liveness SIGTERMs fluent-bit
(exitCode 0, not OOM) into a restart loop.

**Fix**

Relax both probes in the first-party umbrella `values.yaml`
`fluent-bit:` block: `initialDelaySeconds 15, periodSeconds 10,
timeoutSeconds 5, failureThreshold 6` (~75s tolerance from container
start, ~60s steady-state). The `httpGet` target is unchanged — path `/`
on port `http` (`:2020`). The change is values-only; the vendored
subchart is not touched.

**Why load-triggered (not always-broken)**

A steady-state cluster runs the identical probe config with the
DaemonSet 3/3 Ready, 0 restarts, 26 days. The probe only fails when the
`:2020` bind is delayed by API unreachability under install-time load.
Steady-state-stable + flaky-only-under-load = probe-too-aggressive, not
a target or config error.

**Test**

Added a helm-unittest (`tests/fluent-bit_probes_test.yaml`) pinning both
the relaxed timing and the preserved `:2020` httpGet target (path `/`,
port `http`) on the rendered DaemonSet, so a future change that
re-tightens the probes or breaks the path/port is caught.

### Screenshots

N/A — no UI changes.

### Release note

```release-note
fix(monitoring-agents): relax fluent-bit liveness/readiness probes so the DaemonSet no longer crash-loops on a loaded install when its :2020 HTTP server binds late behind a busy kube-API
```


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

## Summary by CodeRabbit

* **Bug Fixes**
* Adjusted Fluent Bit health probes to use more relaxed timing, reducing
the chance of restart loops on busy installations.
* Kept the readiness check pointed at the expected HTTP path and port
while making probe behavior more stable.
* **Tests**
* Added test coverage to verify the generated probe configuration stays
consistent.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 19:22:08 +03:00
IvanHunters 7801f7748e fix(linstor): set controller resource requests and relax liveness probe (#3043)
## What this PR does

Under node CPU contention the host `linstor-controller` JVM cannot
answer its `GET /health` liveness probe within the piraeus-operator
default timing (`timeoutSeconds: 1`, `failureThreshold: 3`). The kubelet
then liveness-kills the container (exit 137, reason `Error`, not
`OOMKilled`). The resulting crash-loop leaves the `linstor-controller`
Service with no ready endpoints, `linstor-csi` gets `EPERM`, and
RWX-Filesystem `DataVolume`s never bind — breaking tenant Kubernetes /
NFS provisioning.

The operator-generated controller pod sets no `resources` (so no CPU
request and no scheduling/CPU guarantee) and ships the tight default
probe. This change, on the `linstor-controller` container in
`spec.controller.podTemplate`:

- adds `resources.requests` (cpu `250m`, memory `700Mi`) — requests
only, no limits: a CPU request guarantees CPU shares so the JVM keeps
answering `/health` under contention, while omitting limits avoids
throttling the JVM or OOM-killing it. The memory value is a conservative
starting estimate; tune it to observed RSS.
- relaxes the liveness timing to `timeoutSeconds: 5` and
`failureThreshold: 6` so a JVM GC pause or CPU contention does not trip
a liveness kill.

piraeus-operator merges `spec.controller.podTemplate` into the generated
Deployment as a strategic merge patch, so the liveness override carries
only the two timing fields; the operator's `httpGet` target (`GET
/health` on the api port) is left intact to avoid pinning a wrong path
or scheme. Both knobs are exposed under `controller.*` in `values.yaml`
for tuning.

This is a latent chart weakness amplified by load, not a regression: on
the same branch, image, and chart, a green run had the controller stable
at `restartCount=0`, while a CPU-loaded run on identical code
crash-looped. Same code, different outcome under load.

### Screenshots

N/A — no UI changes.

### Release note

```release-note
fix(linstor): give the linstor-controller CPU/memory requests and a more tolerant liveness probe so it no longer crash-loops under node CPU contention, which previously left the controller Service with no endpoints and blocked RWX volume provisioning
```


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

## Summary by CodeRabbit

* **New Features**
* Added default controller resource requests for the Linstor controller.
* Added configurable liveness probe timing settings for the controller
container.

* **Tests**
* Added coverage to verify the controller pod template applies the
expected resource requests and liveness probe timing values.
* Confirmed the controller image remains correctly pinned while the new
settings are applied.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 19:22:00 +03:00
IvanHunters 84345da93a fix(opensearch-operator): gate leader election on replica count (#3040)
## What this PR does

**Problem**

opensearch-operator installs intermittently fail with Helm reporting the
controller-manager `Deployment status: 'Failed'`. The real driver is the
manager container (`operator-controller-manager`) crashlooping on
`leader election lost`: controller-runtime calls `os.Exit(1)` when it
misses a lease-renewal deadline under the apiserver/etcd latency that
install-time load creates. The Deployment runs a single replica
(`replicas: 1`) yet still passes `--leader-elect`, so leader election
adds no availability — its only effect is to let a transient
control-plane blip self-terminate the sole manager. The kube-rbac-proxy
`BackOff` seen alongside is collateral restart-window noise, not the
cause: on a long-running cluster the proxy shows `restartCount: 0` while
the manager has restarted repeatedly with `exitCode: 1`, `reason:
Error`.

**Fix**

Gate `--leader-elect` on the replica count instead of passing it
unconditionally. A new `manager.replicaCount` value (default `1`) drives
both `spec.replicas` and the flag: at one replica the manager runs
without leader election and rides out control-plane latency instead of
crashlooping; setting `replicaCount > 1` re-enables `--leader-elect`
automatically so a genuine HA deployment still coordinates. Because both
derive from one value, there is no window where multiple managers run
without a lease.

The same single-replica path also sets the Deployment strategy to
`Recreate`. With leader election off, a default `RollingUpdate` would
briefly run two managers (maxSurge) during an operator upgrade, both
reconciling without lease coordination; `Recreate` closes that window at
zero availability cost on one replica. Installs scaled past one replica
keep `RollingUpdate`.

The arg is hardcoded in the vendored opensearch-operator 2.8.0 chart, so
the change ships as
`packages/system/opensearch-operator/patches/leaderElection.diff`,
re-applied by the package `make update` target after `helm pull
--untar`. This mirrors the existing fluxcd-operator patch mechanism so
the customization survives a chart refresh. A helm-unittest suite pins
both states: the default render drops `--leader-elect` and sets
`strategy: Recreate`; a `replicaCount: 2` render restores
`--leader-elect` and leaves the strategy at the RollingUpdate default.

### Screenshots

Not applicable — no UI change.

### Release note

```release-note
fix(opensearch-operator): disable leader election on the single-replica operator Deployment so a transient apiserver/etcd latency spike no longer crashloops the manager at install time; leader election re-enables automatically when scaled past one replica
```


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

## Summary by CodeRabbit

* **New Features**
* Added configurable replica count for the operator manager, with a
default of 1.
* Single-replica deployments now use a safer rollout strategy and skip
leader election.
  * Multi-replica deployments automatically enable leader election.

* **Tests**
  * Added coverage for single- and multi-replica deployment behavior.

* **Documentation**
* Updated configuration notes to explain replica count behavior and
related deployment changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 19:21:42 +03:00
Aleksei Sviridkin 13f1f9ece1 feat(api): add network.cozystack.io ExposureClass and ServiceExposure
Introduce a new network.cozystack.io/v1alpha1 API group with two kinds
that form the mechanism-agnostic LoadBalancer exposure model:

- ExposureClass (cluster-scoped): the admin binds a logical class to a
  concrete backend (externalIPs, metallb, cilium, robotlb) and its VIP
  address scope, following the StorageClass / IngressClass pattern.
- ServiceExposure (namespaced): the tenant-facing intent to expose a
  Service externally via a named class, with status reporting the
  resolved backend and assigned IPs.

Wire api/network into the controller-gen CRD generation so deepcopy and
the two CRD manifests are produced alongside the existing groups.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-25 17:57:38 +03:00
Aleksei Sviridkin 69247ba80c feat(platform): deploy the securitygroup-controller
Add the securitygroup-controller Helm package (Deployment, ServiceAccount,
ClusterRole and binding, image build) and register it as a default system
package so it ships with the platform.

The ClusterRole grants cluster-wide pod patch — the controller stamps the
SecurityGroup membership label on managed-app pods across dynamically-created
tenant namespaces, so the grant cannot be namespace-scoped — plus read and
finalizer patch on the backing CiliumNetworkPolicies, and leader-election
leases. Tenants gain no new permissions; the membership label is written
exclusively by this platform controller. helm-unittest pins the pod-patch and
ciliumnetworkpolicies grants so a dropped verb fails the suite rather than
silently failing closed at runtime.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-25 17:50:33 +03:00
Aleksei Sviridkin bfc1fbd555 fix(seaweedfs): raise master probe timeouts above the cluster-status backoff
The master's /cluster/status:9333 handler calls topo.Leader(), which spins
an exponential backoff with MaxElapsedTime=20s before returning 200 whenever
the node has no leader in its view — e.g. a master that was briefly the initial
raft leader during bootstrap and then was demoted. The chart's master liveness
and readiness probes both httpGet /cluster/status with timeoutSeconds: 10, which
is shorter than that worst-case backoff. Such a master is then liveness-killed
roughly every 137s into a permanent CrashLoop, the master StatefulSet never
reaches full readiness, and the helm --wait install times out.

Raise both master probe timeouts to 30s so each probe outlasts the 20s backoff
(the volume component already uses 30s). The override sets only timeoutSeconds
and relies on Helm's deep-merge to keep the rest of the subchart probe defaults.
Both probes must be raised: leaving readiness at 10s would keep the master from
becoming Ready and still stall the StatefulSet.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-25 13:39:34 +03:00
Kirill Ilin a411da4aa8 fix(keycloak): honor wildcard cert mode on the admin Ingress
The admin Ingress always requested a per-host ACME certificate, ignoring
the operator wildcard mode that the login Ingress already respects. When
the admin route shares the public ingressClass, the operator default SSL
cert covers it too, so skip per-host ACME there. A separate admin ingress
keeps its own certificate, since that controller has no wildcard default.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-06-25 15:30:19 +05:00
Kirill Ilin 1c8f0243cf feat(keycloak): isolate admin endpoints to a dedicated route
When ingress.adminHost is set, narrow the public (login) route to the
OIDC paths only (/realms, /resources, /.well-known) so the admin console
and Administration REST API are no longer served on the login hostname,
and serve them from a dedicated admin route instead. The admin route
attaches to the Gateway/ingressClass selected by _cluster.expose-ingress-admin
(defaults to expose-ingress), so it can be moved onto a separate, e.g.
private, Gateway. KC_HOSTNAME_ADMIN alone is not a security boundary;
the path and network separation here is what restricts access.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-06-25 15:29:46 +05:00
Kirill Ilin 08dca137de test(keycloak): cover admin console hostname split
Add helm-unittest suite asserting KC_HOSTNAME_ADMIN env, the extra
Ingress rule/TLS entry, and the appended HTTPRoute hostname are emitted
only when ingress.adminHost is set. Wire up the chart 'test' target.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-06-25 15:28:47 +05:00
Kirill Ilin 1df0e2d903 feat(keycloak): add opt-in separate admin console hostname
Add ingress.adminHost option to serve the Keycloak admin console and
admin REST API on a dedicated hostname, separate from the login/realm
hostname. When set, KC_HOSTNAME_ADMIN is configured and an additional
Ingress rule / HTTPRoute hostname routes the admin domain to the same
Keycloak service. Empty by default, keeping current behavior.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-06-25 15:28:47 +05:00
Kirill Ilin bc52d81545 feat(keycloak): allow overriding the database host/port/properties
Make the Keycloak database connection target configurable so it can be routed
through an external endpoint (for example a pgwire proxy) without changing the
chart:

  - db.host / db.port override the host/port otherwise read from the
    keycloak-db-app secret;
  - db.urlProperties sets KC_DB_URL_PROPERTIES (previously never set), e.g.
    sslmode=verify-full&prepareThreshold=0&sslrootcert=...;
  - extraVolumes / extraVolumeMounts allow mounting a CA for sslrootcert.

All new values default to empty, so the rendered StatefulSet is byte-for-byte
unchanged for existing installs.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-06-25 15:28:12 +05:00
myasnikovdaniil fef9e76505 ci(renovate): pin base images and GitHub Actions to digests (#3025)
## What this PR does

Raises OpenSSF Scorecard **Pinned-Dependencies** (currently **1/10**) by
having Renovate manage and pin container base images and GitHub Actions
to immutable digests.

[PR #2849](https://github.com/cozystack/cozystack/pull/2849) SHA-pinned
actions in 13 workflows, but the Scorecard check also scores **container
base images** — 58 unpinned `FROM` lines across **27 distinct images** —
which dominate the score and keep it at 1/10. #2849 also left
`build-main.yaml` and `release-e2e.yaml` actions unpinned, and nothing
in the Renovate config kept the newly-added action SHAs updated (they
were set to rot).

### Changes

- **`.github/renovate.json`**
- Add `dockerfile` and `github-actions` to `enabledManagers` (was
`gomod`-only).
- `pinDigests: true` for both managers — Renovate opens the initial "pin
to `@sha256:`" PR (including the two workflows #2849 missed) and keeps
digests fresh thereafter.
- Batch the initial pin + all digest-only refreshes into a **single
weekly PR** (`matchUpdateTypes: ["digest", "pinDigest"]`, scheduled
Monday). Frequently-rebuilt bases (alpine, debian, golang, distroless)
would otherwise trickle in one rebuild-triggering PR each and load the
shared CI runner. Real tag bumps (major/minor/patch) are intentionally
excluded from the group so they get individual review.
- Existing `do-not-merge/hold` label + `automerge: false` are unchanged,
so every PR still requires a manual merge.

- **`packages/system/bucket/images/s3manager/Dockerfile`** — pin
floating tags to concrete versions so digest tracking doesn't churn
endlessly: `golang:1` → `golang:1.26`, `alpine:latest` → `alpine:3.24`.

### Notes

- The bulk digest pin of the 27 images is left to Renovate's first run
(one grouped PR) rather than hand-edited here, keeping this PR to the
config + tag-hygiene prerequisites.
- Expected effect once the follow-up pin PR merges: Pinned-Dependencies
**1 → ~7–8**. The remaining `downloadThenRun` / pip / npm items cap it
below 10.

### Release note

```release-note
ci(renovate): enable Renovate digest pinning for Dockerfile base images and GitHub Actions to improve the OpenSSF Scorecard Pinned-Dependencies score
```


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

## Summary by CodeRabbit

* **Chores**
* Improved automated dependency management for container images and
GitHub Actions, including clearer update grouping and digest pinning.
* Updated container base images to newer, fixed versions for more
predictable builds and deployments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 15:02:28 +05:00
Aleksei Sviridkin 8e133a7538 fix(kubevirt): update KubeVirt to v1.8.4 (#2940)
## What this PR does

Updates kubevirt-operator from v1.8.2 to v1.8.4. This backports the VMI
checksum status-field validation fix (uint32 range) that Kubernetes 1.36
requires — without it, strict CRD numeric-format validation leaves VMIs
stuck in `Scheduled`. The release also carries assorted bug fixes and
CVE remediations. The v1.7.x line is skipped (live-migration regression,
kubevirt/kubevirt#16386).

The KubeVirt custom resource is unchanged — upstream ships a
byte-identical CR across these versions.

Verified on a dev cluster: all virt-* components roll out to v1.8.4 and
become ready.

Closes #2769

### Screenshots

Not applicable — no UI changes.

### Release note

```release-note
fix(kubevirt): update KubeVirt to v1.8.4; fixes VMIs stuck in Scheduled on Kubernetes 1.36 (strict CRD numeric-format validation of VMI checksum status fields)
```


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

* **Chores**
* Updated the KubeVirt operator version from v1.8.2 to v1.8.4, including
the corresponding deployment configuration and operator
image/environment version bumps.
* Adjusted the operator update process to download the manifest using
v1.8.4 while skipping v1.7.x.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 12:29:00 +03:00
Aleksei Sviridkin baf47ce638 chore(platform): raise Go toolchain to 1.26 across first-party images (#2941)
## What this PR does

Completes the Go-toolchain modernization for first-party images — the
build-time counterpart to #2852, which raised the root and `api/`
modules to Go 1.26.4.

1. **First-party image Go modules → go 1.26.4** (`token-proxy`,
`kube-ovn-webhook`, `kubevirt-csi-driver`): raise the `go` directive and
refresh dependencies. `kube-ovn-webhook` moves `k8s.io/*` 0.26 → 0.36.2
(only long-stable admission/core API is used); `kubevirt-csi-driver`
keeps its existing replace-based k8s alignment at 0.36.0 (main's
refactor owns that module's pin) and raises the `go` directive, with the
`golang.org/x/net` 0.56.0 security bump — which clears the reachable
GO-2026-5026 — reapplied. Adds two `.gitignore` files so the
locally-built binaries can't be committed.
2. **Go builder images → `golang:1.26`** across 8 images still on
EOL/behind toolchains (golang 1.20, 1.20.6, 1.23.4, 1.24, 1.25):
`cluster-autoscaler`, `kubevirt-cloud-provider`, `redis-operator`,
`multus-cni`, `keycloak-operator`, `objectstorage`, `kilo`,
`linstor-csi`. The builder image tag — not the go.mod minimum — sets the
standard-library version baked into the binary, so these still shipped
unpatched stdlib on a plain rebuild.

Verified: `GOOS=linux go build`/`go vet` pass for all three modules and
`go mod tidy` is a no-op; the three biggest builder jumps
(redis-operator, kubevirt-cloud-provider, cluster-autoscaler — all
formerly Go 1.20.x/1.23.x) were verified with a local `docker build`;
the remaining five (1.24/1.25 → 1.26) are covered by CI image builds.

### Release note

```release-note
chore(platform): raise the Go toolchain to 1.26 across first-party images to clear standard-library security advisories
```


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

* **Chores**
* Updated several container build environments to a newer Go release for
consistency across Kubernetes-related components.
* Refreshed module and dependency versions in a few services to keep
builds current.
* Expanded ignore rules for generated image artifacts so build outputs
aren’t tracked.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 12:28:58 +03:00