Commit Graph
2494 Commits
Author SHA1 Message Date
Myasnikov Daniil 91aae36da3 feat(platform): NOTES.txt warns when no-delete VAP is silently skipped
Address review feedback from @lexfrei on packages/core/platform/templates/
deletion-protection.yaml: on a pre-1.30 cluster the capability gate
(`.Capabilities.APIVersions.Has "admissionregistration.k8s.io/v1/
ValidatingAdmissionPolicy"`) evaluates to false and the policy + Binding
render to nothing. There was no signal in `helm install`/`helm upgrade`
output that the guardrail had been silently skipped, so operators on
older clusters lost the protection without any way to discover that from
the install log. Compounded the integration-test gap @lllamnyp called
out in blocker 3.

Add packages/core/platform/templates/NOTES.txt that renders a multi-line
WARNING whenever the capability is absent: states the guardrail is NOT
active, explains why (1.30+ required), notes that the label is still
stamped on every protected object so the guardrail becomes active
automatically on the next upgrade after a cluster bump, no migration
step required.

Add packages/core/platform/tests/notes_capability_gate_test.yaml: positive
case asserts the warning text + the 1.30 hint + the label string render
on a chart-level `capabilities.apiVersions: []`. Negative case asserts
the warning is absent when admissionregistration.k8s.io/v1/
ValidatingAdmissionPolicy is in the capability list. Verified the
positive case fails when the warning text is removed from NOTES.txt.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 15:12:53 +05:00
Myasnikov Daniil 919a8a9d05 docs(platform): clarify sequential self-protection on no-delete VAP
Address review feedback from @lexfrei on packages/core/platform/templates/
deletion-protection.yaml: both the ValidatingAdmissionPolicy and the
ValidatingAdmissionPolicyBinding carry the no-delete label so each
individually requires the bypass dance, but the labeling is symmetric, not
truly bilateral. Removing the label from the Binding and deleting it first
leaves the VAP labeled but unenforced, so the VAP itself is then deletable
without the bypass — and vice versa. That is by design: the guardrail is
operational guidance, not adversarial defense against an operator with
cluster-admin who is determined to remove it.

Add a paragraph in the template comment header so a future reader does not
waste time wondering why the symmetric self-protection isn't truly
bilateral. Forward-references the whole-chart teardown recipe (make
unprotect) so the comment links to the documented escape hatch.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 15:12:53 +05:00
Myasnikov Daniil e34eeb0ee7 fix(platform): document --namespace bypass and add VAP unit tests
Address review feedback from @lllamnyp on packages/core/platform/templates/
deletion-protection.yaml:

The bypass instructions in both the template-author-facing comment and the
user-facing CEL message did `kubectl label <kind> <name>` with no
--namespace flag. Several protected objects are namespaced (cozystack-version
ConfigMap, cozystack-packages Repository, tenant-root HelmRelease), so an
operator who copies the documented command verbatim against their default
context gets a "not found" error and concludes that the bypass mechanism is
broken. Updated both call sites to show --namespace <ns> with an explicit
"(omit for cluster-scoped resources)" caveat.

The reviewer also flagged that the VAP contract is otherwise unasserted by
unit tests. Add packages/core/platform/tests/deletion_protection_test.yaml
covering:
  - both the VAP and the Binding carry platform.cozystack.io/no-delete=true
    on themselves (the self-protection contract)
  - failurePolicy is intentionally absent on the VAP
  - the deny message contains the bypass command WITH --namespace and the
    cluster-scoped caveat (regression catch for this fix; verified failing
    when --namespace is dropped from the template)
  - the Binding's objectSelector + validationActions: [Deny] still match
    the no-delete label

Wire packages/core/platform/Makefile's test target to helm unittest so
hack/helm-unit-tests.sh actually runs the new suite in CI (the existing
tests/ files were orphaned without it).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 15:12:53 +05:00
Myasnikov Daniil ecdf3f3a15 fix(platform): drop redundant failurePolicy on no-delete VAP
Address review feedback from @lllamnyp on packages/core/platform/templates/
deletion-protection.yaml: the VAP's validation expression is the literal
`false` — no parameters, no variables, no references — so CEL evaluation
cannot fail and the api-server never has occasion to apply failurePolicy.
The previous `failurePolicy: Fail` line was a no-op masquerading as
operational config.

Drop the field (the default is Fail anyway in admissionregistration.k8s.io/v1)
and document in the template comment why it is intentionally absent, so the
next reader does not re-add it on the assumption that it was forgotten.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 15:12:53 +05:00
Myasnikov Daniil 052f070b63 fix(platform): migration 42 uses a single declarative apply
Address review feedback from @lllamnyp on packages/core/platform/images/
migrations/migrations/42: the previous version did `kubectl label --overwrite`
then `kubectl apply` on a manifest with no labels, relying on `kubectl apply`'s
3-way strategic merge to leave the imperatively-set label intact. That works
in the steady state but is fragile — the contract is "the label is preserved
because it is never in last-applied-configuration" — and it is inconsistent
with the helm template at templates/cozystack-version.yaml, which bakes the
label inline.

Replace the two-step dance with one declarative apply that includes the
deletion-protection label, matching the chart template. One source of truth,
no last-applied-merge subtlety. The script remains idempotent (apply is a
no-op once the ConfigMap matches).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 15:12:53 +05:00
Myasnikov Daniil c7227bc666 feat(platform): migration 42 backfills no-delete label on cozystack-version
Address review feedback from coderabbitai on
packages/core/platform/templates/cozystack-version.yaml:10:
existing clusters skipped the label because the template's
`lookup`+`if not $configMap` guard never re-renders the ConfigMap on
upgrade (the skip is intentional — it prevents helm from clobbering the
data.version that the migration controller writes from its pre-upgrade
hook).

A one-shot migration is the correct mechanism here:

  - Migration 42 in packages/core/platform/images/migrations/migrations/42
    runs `kubectl label --overwrite configmap cozystack-version
    platform.cozystack.io/no-delete=true` and stamps version=43. Slot 42
    rather than 40 because main already occupies 40 (dashboard CRD
    cleanup) and 41 (kubernetes ephemeralStorage→diskSize rename).
  - Bump migrations.targetVersion 42 -> 43 so the hook fires on the
    next platform upgrade.

Idempotent: --overwrite is a no-op when the label is already at the
target value, MIGRATION_DRY_RUN=1 prints without applying, and the
script aborts before stamping if the kubectl label call fails.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 15:12:52 +05:00
Myasnikov Daniil e5dd5681c3 feat(platform): self-protect the deletion-protection VAP and Binding
Address review feedback from gemini-code-assist on
packages/core/platform/templates/deletion-protection.yaml:15:
label the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
themselves with platform.cozystack.io/no-delete=true so the guardrail
guards itself. A direct kubectl delete on either resource now returns
the same denial as deletion of any other protected object; the same
two-step label-then-delete bypass applies when intentional removal is
needed.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 14:18:22 +05:00
Myasnikov Daniil 43ef456d84 feat(platform): gate deletion-protection VAP on Kubernetes 1.30+ capability
Address review feedback from gemini-code-assist on
packages/core/platform/templates/deletion-protection.yaml:42:
wrap the ValidatingAdmissionPolicy and Binding in a Helm capability
check so the template stays renderable on clusters that pre-date 1.30
(when ValidatingAdmissionPolicy graduated to GA). The check targets the
specific Kind, not the API group/version — admissionregistration.k8s.io/v1
itself has existed since 1.16 for ValidatingWebhookConfiguration, so the
group/version alone is not a meaningful capability gate.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 14:18:22 +05:00
Myasnikov Daniil 0c17d1870c feat(platform): add deletion-protection guardrail via ValidatingAdmissionPolicy
Block accidental DELETE on critical platform objects (tenant-root,
cozy-system, LinstorCluster, CRDs, ClusterIssuers, cozystack-version
ConfigMap, cozystack-packages OCIRepository, tenant-root HelmRelease)
via a label-scoped ValidatingAdmissionPolicy. No webhook DaemonSet,
Service, TLS, or image — the API server evaluates the CEL expression
in-process. Requires Kubernetes 1.30+.

Bypass for legitimate deletes is two steps:
  kubectl label <kind> <name> platform.cozystack.io/no-delete-
  kubectl delete <kind> <name>

CRDs installed dynamically by the operator get the label stamped in
internal/crdinstall/install.go before the apply call.

Release note:
Added deletion-protection guardrail (ValidatingAdmissionPolicy) blocking
DELETE on cozystack platform objects labeled platform.cozystack.io/no-delete=true.
Bypass by removing the label first.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-26 14:18:22 +05:00
Aleksei Sviridkin 92000f1618 fix(kubernetes): add config_path patch for containerd 2.x (#2723)
What this PR does
This PR adds support for containerd 2.x CRI config_path configuration,
as the CRI plugin section was renamed from io.containerd.grpc.v1.cri to
io.containerd.cri.v1.images.

It also makes the sed patching regexes more resilient by matching any
quotes (single or double) around the plugin names, which fixes the issue
on Ubuntu 24.04 (Noble) where containerd 2.x uses single quotes:
[plugins.'io.containerd.cri.v1.images'.registry].

Release note
fix(kubernetes): add config_path patch for containerd 2.x

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

* **Chores**
* Broadened Kubernetes cluster bootstrap handling to better support
different containerd plugin layouts, improving compatibility when
configuring container registry paths during cluster initialization.
* **Tests**
* Added regression tests and fixtures to validate the containerd
configuration patching logic and integrated the check into the
Kubernetes test target.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2723?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-26 11:04:39 +03:00
Aleksei Sviridkin 3756f5c459 feat(networking): Gateway API support via Cilium (supersedes #2213) (#2470)
## What this PR does

Replaces ingress-nginx as the default ingress layer for cozystack-native
services with **Cilium Gateway API**, materialised per-tenant via a new
`TenantGateway` CRD reconciled by `cozystack-controller`. Supersedes
#2213 / #2208. Stacked on top of #2468.

## Architecture

The chart renders one `gateway.cozystack.io/v1alpha1 TenantGateway` CR
per tenant that owns its own Gateway. The cozystack-controller
reconciles the actual `Gateway`, per-tenant `Issuer`, `Certificate`
(wildcard or per-listener), and HTTP→HTTPS redirect `HTTPRoute` from
there. Helm does not render `Gateway` or `Certificate` directly — that
prevents the Helm-vs-controller race on `Gateway.spec.listeners` that
route-driven listener additions would otherwise cause.

```
┌── TenantGateway CR (rendered by extra/gateway chart for owning tenants)
│
└──▶ cozystack-controller reconciles:
        ├─ Gateway (with dynamic listeners, label-based allowedRoutes)
        ├─ Issuer (per-tenant ACME account)
        ├─ Certificate(s) — wildcard + per-child SANs in DNS-01, per-listener in HTTP-01
        ├─ HTTPRoute (controller-owned http→https redirect)
        ├─ Namespace label patching (cozy-* system namespaces from spec.AttachedNamespaces)
        └─ Watches HTTPRoute / TLSRoute attachments to materialise listeners
```

## Inheritance: tenants attach by default, opt into their own Gateway

A tenant gets a dedicated Gateway (own LB Service, own LB IP, own
Certificate) only when it explicitly asks via
`tenant.spec.gateway=true`. Every other tenant in the tree publishes
through the Gateway of the nearest ancestor that owns one — same shape
as the existing `_namespace.ingress` inheritance.

The `apps/tenant` chart writes a `namespace.cozystack.io/gateway` label
on each tenant namespace, carrying either the tenant's own name (when
owning a Gateway) or the inherited ancestor name (when inheriting). The
Gateway's listener `allowedRoutes.namespaces.selector` is keyed on that
label, so the same selector admits routes from every namespace pointing
at the owner — descendants and cozy-* system namespaces alike.
cozystack-controller patches the same label onto every namespace in
`tgw.Spec.AttachedNamespaces` and garbage-collects labels it wrote when
an entry is removed from the attach list (annotation-gated GC so labels
written by the apps/tenant chart are never stripped).

In DNS-01 mode, the controller extends the parent's wildcard Certificate
with `<child-apex>` + `*.<child-apex>` SANs per inheriting child, and
adds a `*.<child-apex>` HTTPS listener per child apex referencing the
same cert. Without this expansion the parent's single-level wildcard
cannot match a child route's hostname (`harbor.alice.example.org` is two
labels past the parent's `*.example.org`).

Opt-out into own Gateway is the right choice when: a tenant needs its
own LB IP (DNS pinned, firewall rule), the apex is not derived from the
parent (custom `host` value the ancestor's cert can't cover), or the
tenant wants its own ACME account / cert authority. Otherwise leave
`gateway` unset and inherit.

## End-to-end flow

1. Platform: `gateway.enabled=true` toggles cert-manager solver,
switches per-system-app templates from Ingress to HTTPRoute / TLSRoute.
2. Per tenant: explicit `tenant.spec.gateway=true` renders a
`TenantGateway` CR and triggers the controller to materialise the owner
Gateway. Tenants with the field unset inherit through
`_namespace.gateway` propagation in the tenant chart.
3. Adding a published app under any tenant (owner or inheriting) is just
deploying its HTTPRoute. The controller picks up the hostname, adds a
per-listener Certificate (HTTP-01) or extends the wildcard cert's SANs
(DNS-01), and updates the owner Gateway's listeners.

Defaults stay legacy: `gateway.enabled=false`,
`tenant.spec.gateway=false`. Existing clusters upgrade unchanged.

## External IP allocation — mechanism-agnostic

The per-tenant Gateway's auto-created `LoadBalancer` Service draws its
IP from whatever LB allocator the cluster admin has configured at the
platform layer — same shape as ingress-nginx today. The tenant API stays
mechanism-agnostic — no `gatewayIP` field, no allocator-specific
manifest in the tenant chart. Cozystack itself ships MetalLB installed
but does not render any `IPAddressPool` / `L2Advertisement` /
`BGPAdvertisement` from this chart; admins set up the allocator that
suits their environment (MetalLB pool with L2 / BGP, Cilium LB-IPAM with
announcer, robotlb against a cloud provider, or
`Service.spec.externalIPs` pinning).

If a tenant needs a specific address (DNS already pinned, firewall rule,
etc.), the operator pre-allocates it on the admin side: either
pre-create the Service with `loadBalancerIP` set, or hand the tenant a
reference to a named admin-managed pool. Per-Service IP uniqueness is
the allocator's responsibility — same as for any other LoadBalancer
Service.

## Cert mode: HTTP-01 (default) vs DNS-01 (opt-in)

`publishing.certificates.solver` selects how the controller sources TLS
certs.

**HTTP-01 (default)** — per-app HTTPS listener + per-app Certificate.
New apps require zero platform-side config: deploy the HTTPRoute,
controller does the rest. Inheriting children's hostnames pick up
per-listener certs the same way the owner's own apps do.

**DNS-01 (opt-in)** — single wildcard Certificate (`<apex>` +
`*.<apex>`) covering every published app under the apex, plus per-child
SANs (`<child-apex>` + `*.<child-apex>`) for every inheriting tenant.
Pick this for clusters where many apps share the apex and Let's Encrypt
rate limits matter. The DNS provider account must be able to write TXT
records under every apex level the parent serves — for deeply-nested
inherited children that requires zone delegation or a wide-scope
provider credential.

| `publishing.certificates.dns01.provider` | Required
`publishing.certificates.dns01.<provider>` keys |
| --- | --- |
| `cloudflare` (default) | `cloudflare.secretName`,
`cloudflare.secretKey` |
| `route53` | `route53.region`, `route53.secretName` (and `accessKeyID`
if not on IRSA) |
| `digitalocean` | `digitalocean.secretName` |
| `rfc2136` | `rfc2136.nameserver`, `rfc2136.tsigKeyName`,
`rfc2136.secretName` |

The platform chart wires every provider's keys into `_cluster.dns01-*`;
the per-tenant gateway chart and the cluster-wide `cluster-issuers.yaml`
both read them.

## Strengthens admission coverage on apps.cozystack.io/*

The `cozystack-tenant-host-policy` probe in `gateway.bats` initially
looked like a flake — the apply that was supposed to be VAP-rejected
just succeeded. Investigation traced it to the custom REST handler at
`pkg/registry/apps/application/rest.go`. genericapiserver hands every
storage method a callback into the validating admission chain:
`createValidation` for Create, `updateValidation` for Update,
`deleteValidation` for Delete. `Update` was correctly invoking its
callback since 23e399bd, but `Create` and `Delete` accepted the
parameter and never called it — admission was silently bypassed for both
verbs on every `apps.cozystack.io` resource (Tenant, MariaDB, Postgres,
Kubernetes, …).

This PR wires the missing `createValidation` and `deleteValidation`
calls into Create and Delete. Every ValidatingAdmissionPolicy and
ValidatingWebhook against `apps.cozystack.io/*` now fires on all three
verbs as the API contract requires. Per KEP-3488 aggregated APIServers
are responsible for enforcing admission on their own kinds;
cozystack-api is configured to do so via
`genericoptions.NewRecommendedOptions`, the only thing missing was the
explicit `*Validation` callbacks in the custom REST.

## Security — five guards, grouped by what they defend

Tenants in Cozystack interact with the platform exclusively through
`apps.cozystack.io/*` resources (Tenant, Bucket, Kubernetes, …) served
by `cozystack-api`. Tenant RBAC (`cozy:tenant:*` aggregated to a
RoleBinding in the tenant's own namespace) does not grant write access
to `gateway.networking.k8s.io/*`, core `Namespaces`, or
`cozystack.io/Package`. The protections below split into three groups by
who they defend against:

**Tenant-user-input gates.** Layer 4 (`cozystack-tenant-host-policy`).
`Tenant.spec.host` is the user-supplied field that surfaces as a
security boundary at the hostname layer; gated on every Create / Update
via `cozystack-api`'s admission chain.

**Defense-in-depth.** Layers 1, 2, 5, 7. These do not protect against
tenant-user input (tenants don't hold the relevant RBAC). They guard
against bugs in cozystack-controller / Flux, supply-chain compromise of
an app chart that emits Gateway API resources, and confused-deputy
mistakes by a cluster admin. Fail-closed via `failurePolicy: Fail` +
`validationActions: [Deny]`.

The five layers themselves:

1. **Listener `allowedRoutes` label selector** —
`namespace.cozystack.io/gateway = <owner-tenant-name>`
(controller-written for cozy-* via `AttachedNamespaces`, Helm-written
for tenant namespaces via the tenant chart, both feeding into the same
Gateway selector).
2. **`cozystack-gateway-hostname-policy`** — VAP on `Gateway`
CREATE/UPDATE. Restricts listener hostnames to the namespace's
`namespace.cozystack.io/host` apex.
4. **`cozystack-tenant-host-policy`** — VAP on `Tenant` CREATE/UPDATE.
5. **`cozystack-namespace-host-label-policy`** — VAP on core `Namespace`
CREATE/UPDATE. Immutability gate on the host label.
7. **`cozystack-route-hostname-policy`** — VAP on `HTTPRoute` and
`TLSRoute` CREATE/UPDATE. Closes the hostname-hijack vector across
namespaces under inheritance — a child cannot claim a hostname outside
its own apex.

The previous Layer 3 (`cozystack-gateway-attached-namespaces-policy`)
and Layer 6 (render-time `fail` in cozystack-basics) banned `tenant-*`
entries in `publishing.gateway.attachedNamespaces` on the assumption
that the attach list was the hijack vector. Under inheritance the attach
surface is the label selector, not the static list; the hijack vector is
closed by Layers 4/5/7 independently. Both Layer 3 and Layer 6 are
dropped — kept in numbering above for continuity with the earlier review
threads.

## Foreign-takeover guards

Five reconcile paths refuse to silently rewrite a pre-existing object
that shares the controller-derived name but carries no `OwnerReference`
back to the TenantGateway: `Gateway`, redirect `HTTPRoute`, per-tenant
`Issuer`, wildcard `Certificate`, per-listener `Certificate`. An
operator who hand-pinned a Certificate or Issuer at the controller's
derived name (private CA, manual cert pinning) gets an explicit
`Ready=False/ReconcileError` condition instead of having their config
silently destroyed and the resource re-issued from a different ACME
account.

The namespace-label patching path applies the same discipline:
cozystack-controller only writes / strips
`namespace.cozystack.io/gateway` on namespaces it annotates with
`cozystack.io/gateway-attached-by`. Labels written by the apps/tenant
chart (no annotation) are never touched, so inheritance for tenant
namespaces survives every reconcile.

## Tests

- **Go controller tests** — 50+ cases in
`internal/controller/tenantgateway/`: idempotency, OwnerReference
cascade, multi-parentRef status, HTTP-01↔DNS-01 mode-transition cert
lifecycle, foreign-takeover refusals (Gateway / HTTPRoute / Issuer /
Certificate / per-listener Certificate), DNS-01 provider matrix
(cloudflare / route53 / digitalocean / rfc2136), HTTPS Kinds=[HTTPRoute]
restriction, fail-closed CEL on missing host label, status condition
wiring. Plus, for inheritance: label-based allowedRoutes selector shape,
namespace label patch + garbage-collect via owner annotation, Helm-owned
label safety, DNS-01 wildcard SAN expansion per inheriting child,
per-child wildcard listener materialisation.
- **Admission-chain tests** —
`pkg/registry/apps/application/rest_admission_test.go` pins
`createValidation`, `updateValidation`, `deleteValidation` paths via
sentinel callbacks.
- **helm-unittest** across `extra/gateway` (TenantGateway CR rendering,
both modes), `core/platform` (DNS-01 wiring), `apps/tenant`
(gateway-effective decision matrix including inheritance),
`system/cozystack-basics` (three VAPs + Layer 7 VAP + tenant-root
namespace label including the conditional
`namespace.cozystack.io/gateway: tenant-root` + cozystack-values-secret
`_namespace.gateway` propagation), `system/cert-manager-issuers` (full
DNS-01 provider matrix), `system/cozystack-api` and `system/dashboard`
(Ingress↔TLSRoute/HTTPRoute toggle), `system/monitoring` (Grafana /
Alerta Ingress↔HTTPRoute toggle).
- **e2e bats** — `hack/e2e-apps/gateway.bats` (17 cases) covering
GatewayClass Accepted, Gateway Programmed + LB Service, HTTPRoute
Accepted with status, VAP rejects cross-tenant hostname hijack, VAP
rejects `tenant.spec.host` writes from non-trusted SA
(regression-detector for the admission-chain fix), VAP rejects
`namespace.cozystack.io/host` label changes from non-trusted SA,
route-hostname VAP rejects HTTPRoute hostnames outside the apex, and
three new cases for inheritance — Package admission accepts `tenant-*`
in `attachedNamespaces` (regression guard for the dropped ban), child
tenant without explicit gateway inherits `_namespace.gateway` from a
Gateway-owning parent in lockstep across the namespace label and the
cozystack-values Secret, and the cross-namespace attach end-to-end where
a child tenant's HTTPRoute reaches Accepted on the parent's Gateway via
the inheritance label.
- **CI wiring** — `make test-controllers` target invoked from
`pull-requests.yaml` runs `go test ./internal/...`.

## Backward compatibility

The only always-on change is the flip of `cilium.envoy.enabled` +
`cilium.gatewayAPI.enabled` to `true` (extra `cilium-envoy` DaemonSet,
~100 MB RAM/node idle). Everything else is gated behind
`gateway.enabled=false` / `tenant.spec.gateway=false`. Existing clusters
see no behavioural change until an operator explicitly opts in.

The cozystack-api admission-chain fix is unconditional. Existing
operators who do not define any VAP / admission webhook on
`apps.cozystack.io` resources see no observable change. Operators who DO
have their own webhooks/VAPs against these kinds will start seeing them
fire on Create and Delete (as intended) — review your policies before
merging if you maintain custom admission for `apps.cozystack.io/*`.

Operators who set `publishing.gateway.attachedNamespaces` to include
`tenant-*` entries used to hit a render-time fail or an admission
rejection — now the apply succeeds and the listed tenant namespaces
simply pick up the gateway-attach label alongside the cozy-* system
namespaces. This matches the inheritance model and was kvaps' explicit
ask in the May 25 review.

## Release note

```release-note
- Adds Gateway API support via Cilium, materialised per-tenant via the new `gateway.cozystack.io/v1alpha1 TenantGateway` CRD reconciled by `cozystack-controller`. Opt-in via `publishing.gateway.enabled=true` at the platform level; per-tenant opt-in via `tenant.spec.gateway=true` for dedicated Gateway / LB IP, otherwise the tenant inherits the nearest ancestor's Gateway via a label-based `allowedRoutes` selector (same shape as the existing `_namespace.ingress` inheritance). Includes HTTP-01 (default) and DNS-01 (opt-in, with cloudflare / route53 / digitalocean / rfc2136 providers) cert solvers; DNS-01 extends the parent's wildcard Certificate with per-child apex SANs and adds a `*.<child-apex>` listener per inheriting tenant. Tenant API is mechanism-agnostic for IP allocation — the LoadBalancer Service draws its IP from the cluster's admin-configured allocator (MetalLB / Cilium LB-IPAM / robotlb / externalIPs).
- Strengthens admission coverage on `apps.cozystack.io/*` resources: `cozystack-api` now invokes `createValidation` and `deleteValidation` callbacks on Create and Delete (Update was already wired). Operators with custom ValidatingAdmissionPolicies or webhooks targeting `apps.cozystack.io/*` will see them fire on all three verbs as the API contract requires; review your policies before merging if you maintain such admission.
```
2026-05-26 10:29:30 +03:00
myasnikovdaniil 2cb16d5ae6 refactor(build): standardize image tagging to fix concurrent PR push conflicts (#2711)
## What this PR does

Concurrent PR builds were racing in the OCIR registry. Every package
Makefile tagged its images via `settag`, which on any non-release commit
collapsed to `:latest` — so every image across every PR was pushed to
the same tag, and parallel builds got 409s on the manifest PUT. Two
outliers (`metallb`, `redis-operator`) used hardcoded version tags and
collided across PRs unconditionally.

This PR replaces `settag` and the `TAG=latest` fallback in
`hack/common-envs.mk` with a single `image-tags` macro driven by three
env vars:

| Env | Meaning |
|---|---|
| `IMAGE_TAG` | Build-unique tag, always pushed |
| `PUBLISH_VERSIONED` | Also push `:<component-version>` (when provided)
|
| `PUBLISH_FLOATING` | Also push `:latest` |

Wired into the two workflows:

- **`pull-requests.yaml`** — `IMAGE_TAG=pr-<N>-<sha>`, both `PUBLISH_*`
off. Every PR build pushes a unique tag set; no collisions, no `:latest`
move.
- **`tags.yaml`** — `IMAGE_TAG=<ref_name>`, both `PUBLISH_*` on. Release
output preserved (component versions + `:latest`) plus a uniform
`:<git-tag>` handle on every image for traceability.
- **Local** — defaults to `IMAGE_TAG=dev`, no flags, `PUSH=0`.

All 30 package Makefiles converted to `$(call
image-tags,<repo>,<versioned-tag>)`. Image consumers reference digests
pinned into `values.yaml` by each Makefile, so the tag is decorative for
cluster-side pulls — the build-unique tag is sufficient for E2E.

Also drops the redundant `:<component-version>-<git-tag>` form (cilium,
linstor, matchbox, ubuntu-container-disk, kubevirt-*,
cluster-autoscaler). Nothing in the repo or downstream values.yaml
consumed it; the same information is preserved via the new `:<git-tag>`
plus the existing `:<component-version>` tags on release.

`ubuntu-container-disk` builds a separate image per Kubernetes version
in a loop, so its primary tag became `:<k8s-ver>-<IMAGE_TAG>` to prevent
loop iterations within a single build from pushing to the same tag.

Verified by dry-running `make -n image` across representative packages
(simple, dual-tag, skopeo, flux-push, loop) under all four contexts (PR
/ release / main / dev) — tag sets are unique and behave as expected.

### Release note

```release-note
refactor(build): PR CI now pushes images under a unique `pr-<N>-<sha>` tag per build instead of `:latest`. Concurrent PR builds no longer race in the registry. Release builds keep the existing `:<component-version>` and `:latest` tags and additionally publish a uniform `:<git-tag>` handle across every image. The redundant `:<component-version>-<git-tag>` tag (cilium, linstor, matchbox, ubuntu-container-disk, kubevirt-*, cluster-autoscaler) is no longer published — image references in values.yaml are digest-pinned and were the only legitimate consumers.
```


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

* **Chores**
* Centralized and simplified container image tagging across build
workflows.
* PR builds now use unique image tags derived from PR metadata for
better isolation.
* Added configurable flags to control publishing of versioned and
floating tags, making release tagging behavior more predictable.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2711?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-25 22:16:57 +05:00
Aleksei Sviridkin afc7765e69 ci(external-dns): regenerate cozyrds schema for gatewayAPI description
`cozyvalues-gen` bakes the values.schema.json's
`gatewayAPI.description` into the JSON openAPISchema field of the
external-dns release-definition CRD at
`packages/system/external-dns-rd/cozyrds/external-dns.yaml`. The Q7
commit (18fd155d2) updated the description from 'HTTPRoute only' to
'HTTPRoute and TLSRoute' but missed running `make generate` in
`packages/extra/external-dns/`, so the cozyrds JSON drifted from
the source values.yaml. Pre-commit drift check caught it on CI.

This commit ships the regenerated cozyrds schema only — no
behavioural change. The 'make generate' rule in extra/external-dns
runs cozyvalues-gen and `hack/update-crd.sh`, both already in
CI's drift verification path.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 19:11:52 +03:00
Aleksei Sviridkin 6e0c2d609d docs(gateway): describe inheritance model and DNS-01 SAN expansion
Replace the 'Inheritance from parent Gateway' known-limitation entry
(which described inheritance as deferred) with:

* A top-of-README 'Inheritance: when to opt in for a separate
  Gateway' section explaining when a tenant owns its Gateway vs
  inherits, and how the namespace.cozystack.io/gateway label is
  written by apps/tenant chart and patched onto cozy-* by the
  controller.
* DNS-01 cert-mode docs spell out SAN expansion for child apexes:
  the wildcard Certificate now carries <child-apex> + *.<child-apex>
  per inheriting tenant, with a per-child *.<child-apex> listener
  on the parent Gateway. Documents the DNS-provider-permission
  requirement for deeply-nested children (ACME challenge writes
  TXT under each apex zone, so zone delegation or wide-scope
  provider creds are needed for grandchild apexes).
* New known-limitation entries: 64-listener cap on the parent
  Gateway under inheritance (with the high-fanout escape via
  tenant.spec.gateway=true), and Cilium sharing-key port-collision
  (multiple per-tenant Gateways still each take their own LB IP
  until ListenerSet ships in Cilium).

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 19:02:22 +03:00
Aleksei Sviridkin 32241afe03 feat(gateway): drop tenant-* ban on gateway-attached-namespaces
Removes two gates that prevented child tenants from attaching to a
parent's publishing Gateway:

1. Render-time fail() in templates/gateway-hostname-policy.yaml that
   refused to render the chart when _cluster.gateway-attached-
   namespaces contained any tenant-* entry.
2. cozystack-gateway-attached-namespaces-policy VAP that enforced
   the same rule at Package CR admission time.

The original rationale — 'a tenant in attachedNamespaces can hijack
the publishing tenant's hostnames' — assumed a static-name
allowedRoutes whitelist where adding tenant-* literally widened the
attach surface for arbitrary hostnames. Under the inheritance
refactor (Stage 1/3/5) the attach surface is governed by the
namespace.cozystack.io/gateway label selector, not by entries in
AttachedNamespaces; and hostname hijack is independently blocked by
three other layers:

* Layer 4 — cozystack-tenant-host-policy VAP on Tenant.spec.host
* Layer 5 — cozystack-namespace-host-label-policy VAP on the
  namespace label
* Layer 7 — cozystack-route-hostname-policy VAP on HTTPRoute and
  TLSRoute hostnames

Those three defend against the hijack vector regardless of what
sits in attachedNamespaces, and they apply uniformly to every
tenant (inheriting or owning a Gateway). The dropped gates added
no extra defence, while their presence broke the inheritance flow
kvaps reasonably asks for in the May 25 review.

Doc count for gateway-hostname-policy.yaml drops from 8 to 6 (one
VAP + one Binding removed); helm-unittest fixtures updated to
match, with a regression-guard test asserting the removed VAP name
no longer appears in any rendered document.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 19:00:31 +03:00
Aleksei Sviridkin 30302eabe4 feat(tenantgateway): switch Gateway allowedRoutes to label selector
The Gateway's HTTPS / TLS-passthrough listeners now match attached
namespaces by the namespace.cozystack.io/gateway label keyed on the
TenantGateway's own namespace, instead of a static
kubernetes.io/metadata.name In [list] whitelist. This restores
inheritance parity with the legacy ingress flow: child tenants of a
Gateway-owning ancestor publish through the ancestor's Gateway by
default — without their own LB IP / Certificate, and without
operator intervention to add each child by name to platform values.

Two writers populate the label:

* apps/tenant chart namespace.yaml — each tenant namespace gets a
  label pointing at the nearest ancestor that owns a Gateway (self
  if owning, inherited otherwise). The tenant Helm chart is the
  source of truth for tenant-tree labels; the controller never
  touches them (annotation-gated GC).
* cozystack-controller (ensureNamespaceLabels) — patches the label
  onto every namespace in tgw.Spec.AttachedNamespaces, so cozy-*
  system namespaces (cert-manager, monitoring, harbor, …) reach the
  publishing Gateway alongside the tenant tree. Tracks its own
  writes via the cozystack.io/gateway-attached-by annotation; on a
  reconcile where an entry is removed from AttachedNamespaces, the
  label is garbage-collected (annotation present + namespace no
  longer expected → strip both label and annotation).

Also adds the gateway-attach label to tenant-root itself via
cozystack-basics/templates/tenant-root.yaml when
_cluster.gateway-enabled is true, so the Gateway in tenant-root's
own namespace passes its own selector for the controller-owned
http→https redirect HTTPRoute.

Tests (TDD, red→green):
* TestReconcile_HTTPSListenerUsesGatewayLabelSelector pins the new
  MatchLabels shape on HTTPS listeners.
* TestReconcile_LabelsAttachedNamespaces pins the patch path: every
  namespace in spec.AttachedNamespaces (plus tgw.Namespace) carries
  namespace.cozystack.io/gateway=<owner> after reconcile.
* TestReconcile_LabelGCRemovesDroppedAttachedNamespaces pins the GC
  path: removing an entry between reconciles strips the label.
* TestReconcile_LabelGCDoesNotStripHelmOwnedLabels pins the safety
  contract: labels lacking the controller annotation (Helm-owned
  tenant labels) are left alone.
* tenant-root_test.yaml adds the positive case for the conditional
  label render under _cluster.gateway-enabled=true.

The HTTP listener (port 80) keeps the narrower static-name selector
— it allows only the tenant namespace plus cozy-cert-manager (for
HTTP-01 ACME challenges) and must NOT accept routes from app
namespaces or child tenants, since unattended routes on the HTTP
listener serve plaintext and leak credentials.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 18:32:20 +03:00
Aleksei Sviridkin 97dff75b5f feat(tenant): inherit _namespace.gateway from parent
Mirror the existing _namespace.ingress inheritance for Gateway API
attachment. A child tenant under a Gateway-owning ancestor publishes
its routes through that ancestor's Gateway by default, without burning
a dedicated LB IP / Certificate per child.

Resolution rule (tenant.gatewayEffective):
  * gateway=true explicit  → own Gateway (LB IP + cert burned)
  * gateway=false explicit → parked (no inherit, no own)
  * gateway unset          → inherit $parentNamespace.gateway

Removes the auto-default for derived-apex tenants — the previous shape
gave every derived-apex child its own Gateway / IP / cert, which scales
linearly with tenant depth and produces ~N×K LB IPs on a tree of N
top-level × K derived-apex tenants. Inheritance collapses that to one
LB IP per Gateway-owning ancestor.

Custom-apex tenants (host explicitly set to a non-derived value) now
fall through to inheritance instead of auto-firing a separate Gateway.
The previous auto-fire conflated 'custom apex' with 'want public
exposure' and would have silently failed TLS termination anyway (the
ancestor's cert does not cover an unrelated apex). The new rule:
explicit gateway=true is the only way to ask for a separate Gateway.

The namespace.cozystack.io/gateway label and _namespace.gateway in the
rendered cozystack-values Secret now both carry the owner-tenant name
— could be self (when this tenant owns one) or any ancestor up the
chain. Apps in the tenant render HTTPRoutes whose parentRefs.namespace
points at that owner.

helm-unittest pinned for the HelmRelease render side. The
cross-namespace attach end of the flow (ReferenceGrants, listener /
SAN expansion on the owner Gateway) lands separately in the
TenantGateway controller.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 18:18:54 +03:00
Aleksei Sviridkin 0994821bda feat(monitoring): toggle ingress↔HTTPRoute on _cluster.gateway-enabled
Adds Gateway API equivalents for the two ingress endpoints in the
monitoring chart — Grafana (web UI) and Alerta (alert console) — gated
on the same _cluster.gateway-enabled flag the keycloak / dashboard /
cozystack-api templates already use.

* Grafana's ingress is configured inline via Grafana CRD's spec.ingress
  block; that block is now wrapped with {{ if ne gateway-enabled true }}
  and a standalone HTTPRoute attaching to the tenant Gateway is rendered
  in the alternate branch.
* Alerta uses a standalone Ingress object; the same Ingress↔HTTPRoute
  swap is wired through an if/else.

Both routes attach to Gateway cozystack in the publishing namespace
(default tenant-root), matching the convention established by the rest
of the system charts.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 17:31:03 +03:00
Aleksei Sviridkin 18fd155d2b feat(external-dns): enable gateway-tlsroute alongside gateway-httproute
The Gateway API source-flag toggle (gatewayAPI=true) already enabled
gateway-httproute discovery. cozystack ships TLSRoute attachments for
SSL-passthrough services (kubernetes control plane API, kubevirt CDI
uploadproxy, kubevirt VM exportproxy) — without gateway-tlsroute in the
external-dns sources list those hostnames are never picked up for DNS
record management.

Adds gateway-tlsroute under the same conditional and updates the value
description / schema / README to reflect that the toggle now covers both
route kinds.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 17:27:14 +03:00
Aleksei Sviridkin 7c893bc653 docs(gateway): security model as three groups, mechanism-agnostic IP allocation
Security model section split into three groups by who they defend
against, anchoring the framing in the apps.cozystack.io/* tenant API
surface (tenants do not write Gateway API resources directly, so most
of the seven layers are not protecting against tenant-user input):

- tenant-user-input gates: Layer 4 (cozystack-tenant-host-policy)
  via cozystack-api admission. Tenant.spec.host is the user-supplied
  field that surfaces as a security boundary.
- defense-in-depth: Layers 1, 2, 5, 6, 7. Cover bugs in
  cozystack-controller / Flux, supply-chain compromise of an app
  chart, confused-deputy admin mistakes. Fail-closed.
- admin-against-themselves: Layer 3 (cozystack-gateway-attached-
  namespaces-policy). Catches kubectl edit on the platform Package.

Layer 7 wording fix: drop the implication that a tenant user with
HTTPRoute RBAC could exploit the cross-apex hostname surface — that
RBAC is not granted in Cozystack. Reframed as defense-in-depth
against an app chart bug or supply-chain compromise.

External IP allocation section reframed as admin-side concern:
allocator (MetalLB / Cilium LB-IPAM / robotlb / externalIPs) is
configured at the platform layer, tenant API stays
mechanism-agnostic. Per-Service IP uniqueness is the allocator's
responsibility — same shape as for any LoadBalancer Service.

Inheritance Known limitations entry: spell out both upstream gaps
(Gateway API 64-listener cap, Cilium sharing-key inactive on
443/TCP via cilium#42756) so readers understand why per-tenant
Gateway is the only scalable shape today.

publishing.gateway.attachedNamespaces -> gateway.attachedNamespaces
fix: the values block is at top-level, not under publishing.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 17:07:02 +03:00
Aleksei Sviridkin 039924f59d feat(gateway): align _namespace.gateway propagation with tenant.gatewayEffective
Two related fixes for the gap where the tenant chart's gateway HelmRelease materialises but child apps (harbor, bucket) silently fall back to ingress-nginx:

- packages/apps/tenant/templates/namespace.yaml: switch the
  _namespace.gateway resolution from raw .Values.gateway to
  tenant.gatewayEffective. This is the same helper consulted by
  gateway.yaml (HelmRelease render), so per-tenant Gateway
  materialisation and child-app HTTPRoute / Ingress selection now
  stay in lockstep — including the auto-default for derived-apex
  tenants.
- packages/system/cozystack-basics/templates/cozystack-values-secret.yaml:
  add a conditional gateway field to the tenant-root cozystack-values
  Secret. cozystack-basics is the only writer of _namespace.* for
  tenant-root (apps/tenant chart skips tenant-root via the existing
  ne 'tenant-root' gate). Without this line, tenant-root apps read
  _namespace.gateway as empty and route to ingress even when the
  platform's gateway.enabled flag is on. Conditional on
  _cluster.gateway-enabled='true' so non-Gateway clusters keep the
  empty value.

Test fixture in cozystack-basics/tests/cozystack-values-secret_test.yaml
pins the conditional and the regression-guard that the other four
_namespace keys (etcd, ingress, monitoring, seaweedfs) stay
unconditional.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 17:07:02 +03:00
Aleksei Sviridkin d2e851f63b feat: Gateway API support via Cilium
Replace ingress-nginx as the default ingress layer for cozystack-native
services with Cilium Gateway API, materialised per-tenant via a new
TenantGateway CRD reconciled by cozystack-controller.

The extra/gateway chart renders one gateway.cozystack.io/v1alpha1
TenantGateway per tenant; the controller owns the resulting Gateway,
per-tenant Issuer, Certificate(s), and the http->https redirect
HTTPRoute. This avoids the helm-vs-controller race on
Gateway.spec.listeners when route-driven listeners are added.

publishing.certificates.solver selects HTTP-01 (default; per-app HTTPS
listener + per-app Certificate) or DNS-01 (opt-in; single wildcard
Certificate per apex; providers: cloudflare, route53, digitalocean,
rfc2136). HTTP-01 keeps zero-config per-app onboarding; DNS-01 saves
Let's Encrypt budget on apex-shared clusters.

Security model -- seven independent layers, all admission-time gates
fail-closed:

  1. Listener allowedRoutes namespace whitelist via the
     kube-apiserver-written kubernetes.io/metadata.name label;
     plain-HTTP listener is strictly narrower than HTTPS, and HTTPS /
     passthrough listeners restrict allowedRoutes.kinds to
     HTTPRoute / TLSRoute respectively.
  2. cozystack-gateway-hostname-policy VAP on Gateway.
  3. cozystack-gateway-attached-namespaces-policy VAP on Package --
     blocks tenant-* in gateway.attachedNamespaces.
  4. cozystack-tenant-host-policy VAP on Tenant -- blocks spec.host
     writes from non-trusted callers.
  5. cozystack-namespace-host-label-policy VAP on Namespace -- blocks
     writes to namespace.cozystack.io/host from non-trusted callers.
  6. Render-time fail in cozystack-basics for tenant-* in
     gateway.attachedNamespaces.
  7. cozystack-route-hostname-policy VAP on HTTPRoute / TLSRoute,
     scoped to tenant-* namespaces.

Within-apex cross-namespace hostname conflict is resolved at reconcile
time by a HostnameConflict condition (cozy-* namespace wins).

Five reconcile paths refuse to rewrite pre-existing objects with the
controller-derived name but no OwnerReference back to the
TenantGateway: Gateway, redirect HTTPRoute, per-tenant Issuer, wildcard
Certificate, per-listener Certificate.

Derived-apex tenants (tenants whose host is a subdomain of a parent
tenant's) auto-enable Gateway when the platform-level gateway.enabled
is on.

Bonus fix: pkg/registry/apps/application/rest.go was ignoring the
createValidation and deleteValidation callbacks that genericapiserver
hands to every storage method, silently bypassing admission for Create
and Delete on every apps.cozystack.io resource (Tenant, MariaDB,
Postgres, Kubernetes, ...). Aligned with the existing updateValidation
pattern.

Always-on changes: cilium.envoy.enabled + cilium.gatewayAPI.enabled
flip to true (extra cilium-envoy DaemonSet, ~100 MB RAM/node idle),
and the cozystack-api admission-chain fix. Everything else is gated
behind gateway.enabled=false and tenant.spec.gateway=false; existing
clusters see no behavioural change until an operator opts in.

Tests: 60+ Go cases in internal/controller/tenantgateway/, admission-
chain regression tests in pkg/registry/apps/application/, helm-unittest
across 7 charts (extra/gateway, core/platform, apps/tenant,
system/cozystack-basics, system/cert-manager-issuers,
system/cozystack-api, system/dashboard), and 14 e2e bats scenarios in
hack/e2e-apps/gateway.bats. New make test-controllers target wired
into pull-requests.yaml.

Deps: sigs.k8s.io/gateway-api v1.4.1, cert-manager v1.17.4.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-25 17:07:02 +03:00
Aleksei Sviridkin cb2f606c76 fix(csi): verify VMI Ready after kubevirt-csi Publish to surface stuck-PVC failures (#2659)
## What this PR does

Stops `ControllerPublishVolume` from silently reporting success when the
volume is in VM spec but the hotplug attach never actually completed.

### Root cause

Upstream's fast path (`EnsureVolumeAvailableVM`) checks only
`vm.Spec.Template.Spec.Volumes`. If a prior Publish wrote that entry but
timed out waiting for VMI Ready (e.g. the infra PVC is stuck
`ClaimPending` because LINSTOR cannot autoplace), the next Publish
short-circuits to success. external-attacher sets `Attached=true`, but
QEMU was never sent `device_add`. The tenant kubelet's NodeStageVolume
then fails with:

```
rpc error: code = Unknown desc = couldn't find device by serial id
```

Observed in production. Sequence from kubevirt-csi-driver controller
logs:

```
16:53:13  /ControllerPublishVolume called
16:55:13  volume failed to be ready in time (2m), context deadline exceeded
16:55:14  /ControllerPublishVolume called  (retry)
16:55:14  Volume X already attached to VM Y - skipping hot-plug
16:55:14  /ControllerPublishVolume returned with response: {}
```

VMI volumeStatus during that window: `phase=Pending message="PVC is in
phase ClaimPending"`. VolumeAttachment then goes `Attached=true`; tenant
kubelet surfaces "couldn't find device by serial id".

### Fix

After upstream Publish returns success for a hotplug volume, the wrapper
re-reads VMI status and checks `Phase == VolumeReady` for this
DataVolume. If it is not Ready (or not present), the RPC returns
`codes.Unavailable` with the upstream reason, so external-attacher keeps
retrying instead of declaring the volume attached. The underlying
provisioning failure surfaces at the CSI layer instead of becoming a
confusing kubelet-side "device not found by serial id".

This is a wrapper-side workaround for a defect in upstream
`kubevirt.io/csi-driver`; the structural fix belongs upstream (tighten
the fast path to require VMI Ready) and will be filed separately. The
wrapper guard is safe to keep as defense-in-depth even after the
upstream fix lands.

Relates to #2634.

### Release note

```release-note
fix(csi): kubevirt-csi-driver Publish now verifies VMI Ready before reporting attached, surfacing stuck-PVC failures at the CSI layer instead of as "couldn't find device by serial id" on the tenant kubelet
```


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

* **Bug Fixes**
* Improved hotplug volume publishing: non‑NFS volumes are now verified
on the VM before being marked published; if the volume is missing or not
ready, publishing returns unavailable, improving reliability for
hotplugged storage.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2659?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-25 17:04:43 +03:00
Aleksei Sviridkin 60357eb4a6 fix(csi): route RWX Block volumes to upstream hotplug detach (#2658)
## What this PR does

Fixes routing in `WrappedControllerService` so RWX **Block** volumes
(KubeVirt live-migration disks) are no longer treated as NFS during
unpublish and expand.

### Root cause

`ControllerUnpublishVolume` selected the NFS cleanup branch based on
`hasRWXAccessMode(pvc)` alone. RWX Block matched and went down the NFS
path — which just deletes a `CiliumNetworkPolicy`. For a block volume
that CNP does not exist, so `removeCNPOwnerReference` returns
successfully without doing anything, and the function reports success
without ever calling upstream's hotplug detach. The DataVolume entry
stays in `VM.spec.template.spec.volumes` permanently. When the same PVC
later attaches to a different VM, linstor-csi's anti-split-brain check
sees pods from two different VMs referencing the same RWX block PVC and
blocks the attach with:

```
RWX block volume <ns>/<pvc> is being used by pods from different VMs (<vm-a> and <vm-b>);
this is not supported - RWX block volumes with allow-two-primaries are only for live migration of a single VM
```

`ControllerPublishVolume` was unaffected because it routes on the
explicit `nfsVolumeKey` from `VolumeContext`.
`ControllerUnpublishVolume` does not receive `VolumeContext` and has to
infer from the PVC, and the previous inference was incomplete.

### Fix

`isNFSVolume(pvc)` now requires `ReadWriteMany` access mode **and**
`VolumeMode == Filesystem` (always set by our NFS `CreateVolume` path).
RWX Block correctly delegates to upstream `ControllerUnpublishVolume`,
which removes the entry from VM spec via `RemoveVolumeFromVM`. The same
predicate replaces the previous access-mode check in
`ControllerExpandVolume`, which was also clearing
`NodeExpansionRequired` for RWX Block — block volumes still need in-VM
disk expansion.

Fixes #2634.

### Release note

```release-note
fix(csi): route RWX Block volumes in kubevirt-csi-driver to upstream hotplug detach so VM spec entries are cleaned up on unpublish
```


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

* **Bug Fixes**
* More accurate detection of network-attached (NFS) vs block volumes,
preventing incorrect cleanup of block volumes.
* NFS volumes continue to receive special cleanup; non-NFS volumes now
follow upstream cleanup paths.

* **Refactor**
* Node-side expansion logic refined to base decisions on improved NFS
classification (RWX + filesystem), reducing expansion errors.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2658?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-25 16:58:55 +03:00
Myasnikov DaniilandClaude Opus 4.7 1bcaa3de54 fix(flux-plunger): correct common-envs.mk include path
The include pointed at ../../../scripts/common-envs.mk, but that
directory does not exist — only hack/common-envs.mk does. flux-plunger
is not part of the root `make build`, so the broken path went unnoticed
until the new image-tags macro made the recipe depend on it.

Without the include, `make -C packages/system/flux-plunger image` fails
immediately with "No such file or directory".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-25 17:45:44 +05:00
Myasnikov Daniil 6c8a67db3d fix(talos): fail fast on skopeo copy errors in image-talos
The image-talos recipe chains skopeo copy with `;` in a single shell.
Without `set -e`, the recipe's exit status is the last command's, so a
failed copy in the middle of the chain can be masked by a later
successful command — or worse, by a false-branch `if`/`fi` (which exits
0 and would silently hide a push failure on PR builds where both
PUBLISH_* flags are 0).

Prepend `set -e;` to abort the recipe on the first failure. Also quote
"$SRC" defensively.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-25 12:58:08 +05:00
Aleksei Sviridkin f874ba93b8 test(kubernetes): functional regression test for config_path seds
The helm-unittest assertions added in the previous commit cover the
structural shape of the sed lines (they must exist in
preKubeadmCommands). They do not catch the failure mode this PR exists
to fix: a future containerd release renames the registry plugin
section, the sed silently stops matching, and per-registry mirroring
under /etc/containerd/certs.d quietly breaks.

Add a bash test that:

1. Drift-checks the script's copy of the two sed lines against the
   verbatim strings embedded in templates/cluster.yaml, so the test
   cannot diverge from the template without failing.

2. Runs both seds against minimal fixtures of containerd 1.x and 2.x
   default configs (tests/fixtures/containerd-{1,2}x.toml) and asserts
   that config_path is rewritten exactly once in each. If containerd
   3.x renames the section again, the fixture stops matching, the test
   fails, and CI surfaces the regression instead of letting it ship
   silently.

Wired into the package's `make test` target, which is already invoked
in CI alongside helm-unittest.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-24 13:58:34 +03:00
Aleksei Sviridkin 47953c48b8 test(kubernetes): cover containerd config_path patching for both plugin schemas
Add helm-unittest assertions for both sed lines in preKubeadmCommands so
that a future regression to a single-regex form, which silently fails
against containerd 2.x where the plugin section was renamed and quoted
with single quotes by default, is caught at template render.

Add a Helm comment above the two seds explaining the rename from
io.containerd.grpc.v1.cri (containerd 1.x) to io.containerd.cri.v1.images
(containerd 2.x), so future readers understand why both lines exist and
that the wrong-version one is intentionally a no-op on each image
version.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-05-24 13:37:21 +03:00
Elaugaste dbcd09a0fd fix(kubernetes): add config_path patch for containerd 2.x
This adds support for containerd 2.x CRI config_path configuration, as the CRI plugin section was renamed from io.containerd.grpc.v1.cri to io.containerd.cri.v1.images. It also supports both single and double quotes around plugin names.

Assisted-By: Gemini <noreply@google.com>
Signed-off-by: Elaugaste <elaugaste@gmail.com>
2026-05-24 13:57:11 +06:00
Andrey Kolkov debb9f85db feat(platform): add implementation of backup-strategy controller for etcd (#2641)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Screenshots

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

### Release note

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

```release-note
add implementation of backup-strategy controller for etcd
```

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

* **New Features**
* Cluster-scoped Etcd backup strategy CRD (S3-only) plus controller
support for BackupJob (snapshot → Backup) and destructive in-place
RestoreJob flows; snapshot metadata persisted and destination validation
enforced.

* **Documentation**
* Complete etcd backup/restore README and admin/tenant scenario guides.

* **Examples & Tests**
* End-to-end demo scripts (helpers, stepper, cleanup, runner), Bats e2e,
and extensive unit tests.

* **Chores**
* RBAC updates for etcd access, chart version bumped, demo env files
added to .gitignore.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2641?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-22 16:06:02 +04:00
Andrey Kolkov d80f67e799 feat(platform): add implementation of backup-strategy controller for etcd
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-05-22 14:35:08 +04:00
myasnikovdaniil e841688082 fix(postgres): accept integer values for postgresql.parameters in schema (#2687)
## What this PR does

PostgreSQL server parameters like `max_connections` are natural
integers,
but the chart's schema declared them as `{type: string}` only. When
users
supply bare integers (e.g. `max_connections: 100`) — either directly in
Application CR specs or via the UI — Helm rejects the values at schema
validation time before template rendering, causing HelmRelease failures.

The Helm template already handles both types via `{{ $value | toString |
quote }}`,
so the fix is to teach the schema to accept both.

### Approach

The schema files (`values.schema.json` and the embedded `openAPISchema`
in `postgres-rd/cozyrds/postgres.yaml`) are generated from `values.yaml`
annotations via `cozyvalues-gen`. Hand-editing them does not survive
`make generate` — same regeneration trap that caught #2466 (issue
#2597).

`cozyvalues-gen` v1.4.0 (cozystack/cozyvalues-gen#25) adds an
`intOrString` type alias for map values that emits
`additionalProperties.anyOf: [integer, string]` plus the Kubernetes
`x-kubernetes-int-or-string: true` marker.

### Changes

- `packages/apps/postgres/values.yaml`: annotation flipped from
  `@field {map[string]string}` to `@field {map[string]intOrString}`,
  description updated to note integer values are accepted.
- Regenerated artifacts: `values.schema.json`, embedded `openAPISchema`
  in `packages/system/postgres-rd/cozyrds/postgres.yaml`,
`packages/apps/postgres/README.md`, and
`api/apps/v1alpha1/postgresql/types.go`
  (Parameters is now `map[string]intstr.IntOrString`).
- `packages/apps/postgres/templates/db.yaml`: `$dangerousParams` list
  extended with `archive_cleanup_command` and `recovery_end_command`
  (same RCE surface as `archive_command` / `restore_command`).

### Verification

- `helm template` with `max_connections: 200` (int) and
`max_connections: "200"`
  (string) produce identical render output.
- Dangerous-param block list rejects `archive_cleanup_command` and
  `recovery_end_command` with the expected `SECURITY:` error.
- `make test` in `packages/apps/postgres/`: 13/13 pass.
- `make generate` is idempotent — schema files don't drift on regen.

### Release note

```release-note
fix(postgres): accept integer values for postgresql.parameters to prevent HelmRelease failures when parameters are supplied as unquoted integers; also block archive_cleanup_command and recovery_end_command (RCE surface).
```

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

* **New Features**
* PostgreSQL parameters now accept integer or string values; integers
are automatically coerced to strings.

* **Security**
* Enhanced parameter filtering blocks additional dangerous settings:
archive_cleanup_command and recovery_end_command.

* **Documentation**
* Updated docs and charts to describe int-or-string parameter support
and revised blocked-parameter guidance.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2687?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-22 08:58:22 +05:00
Myasnikov Daniil c0db80591d fix(csi): tighten isNFSVolume to Filesystem-only
Address review feedback from gemini-code-assist on
packages/apps/kubernetes/images/kubevirt-csi-driver/controller.go:562:
classify a PVC as NFS only when its VolumeMode is Filesystem (or nil,
which defaults to Filesystem), instead of "anything not Block". Future
VolumeMode values will no longer be silently routed to the NFS path.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-21 23:28:43 +05:00
Myasnikov Daniil 712c93da07 fix(csi): route RWX Block volumes to upstream hotplug detach
WrappedControllerService.ControllerUnpublishVolume decided between NFS
cleanup and upstream hotplug detach using only hasRWXAccessMode(pvc).
RWX Block volumes (KubeVirt live-migration disks) match that check and
were routed to the NFS path, which only deletes a CiliumNetworkPolicy.
For a block volume that CNP does not exist, so removeCNPOwnerReference
returns successfully without doing anything, and the function reports
success without ever calling upstream's hotplug detach. The DataVolume
entry stays in VM.spec.template.spec.volumes; when the same PVC later
attaches to a different VM, linstor-csi's anti-split-brain check sees
pods from two different VMs referencing the same RWX block PVC and
rejects the attach permanently.

Introduce isNFSVolume(pvc) requiring both ReadWriteMany access mode and
VolumeMode=Filesystem (always true for PVCs provisioned via our NFS
CreateVolume path). RWX Block now delegates to upstream, which removes
the entry from VM spec via RemoveVolumeFromVM. The same predicate
replaces the access-mode check in ControllerExpandVolume - RWX Block
volumes still need in-VM disk expansion.

Fixes #2634

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-21 23:28:42 +05:00
Myasnikov Daniil cbe4b863ec fix(csi): return gRPC errors when post-Publish VMI readiness check cannot run
The fallback branches in publishHotplugVolume silently returned the
upstream success response when the node ID failed to parse or the VMI
get call returned an error. external-attacher then set Attached=true
without VolumeReady proof, which recreates the false-positive attach
this wrapper exists to prevent.

Fail loudly instead:

- parseErr -> codes.Internal: external-attacher is expected to provide
  a well-formed "namespace/name" node ID per the CSI contract; if it
  does not, surface the contract violation rather than mask it.
- getErr -> codes.Unavailable: this is the same code returned for the
  not-Ready and not-present cases just below, so the retry behavior on
  the attacher side stays consistent. By the time we reach this branch
  the upstream Publish call already succeeded, which means it just
  looked the VMI up; a transient get error here is a race or API hiccup
  and a retry is safe.

Also add a one-line comment on GetVirtualMachine: in the upstream
kubevirt.io/csi-driver client this method returns a
VirtualMachineInstance despite its name, so accessing
vmi.Status.VolumeStatus on the result is type-correct.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-21 23:28:07 +05:00
Myasnikov Daniil 5bb5e95a30 fix(csi): verify VMI Ready after Publish to surface stuck-PVC failures
Upstream ControllerPublishVolume has a fast path that returns success
based only on the volume's presence in VM.spec.template.spec.volumes
(via EnsureVolumeAvailableVM). If a prior Publish wrote that entry but
the volume never became Ready in VMI status - e.g. the infra PVC is
stuck ClaimPending because LINSTOR cannot autoplace - the retry hits
the fast path and reports success. external-attacher marks the
VolumeAttachment Attached=true, but QEMU was never sent device_add, so
the tenant kubelet fails NodeStageVolume with:

    rpc error: code = Unknown desc = couldn't find device by serial id

Wrap upstream Publish for hotplug volumes (non-NFS) with a post-call
check that the volume reached VolumeReady in VMI.Status.VolumeStatus.
When it has not, the RPC returns codes.Unavailable carrying the actual
reason (e.g. "PVC is in phase ClaimPending"), so external-attacher
keeps retrying and the underlying provisioning failure is visible at
the CSI layer rather than as a confusing "device not found by serial
id" on the tenant kubelet.

This is a wrapper-side workaround for an upstream defect in
kubevirt.io/csi-driver; the structural fix belongs upstream (tighten
the fast path to require VMI Ready) and will be filed separately.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-21 23:28:06 +05:00
myasnikovdaniil ee9d1544e1 fix(platform): migrate ephemeralStorage to diskSize via pre-upgrade hook (#2688)
## Problem

PR #2454 renamed `nodeGroups[*].ephemeralStorage` →
`nodeGroups[*].diskSize` with
a hard `{{ fail }}` guard. Any cluster whose HelmRelease still carries
the legacy
field cannot be reconciled by Flux at all — unrelated control-plane
changes and
MachineHealthCheck remediations are also blocked.

## Solution

Add platform migration 41 that runs as a pre-upgrade hook before any
chart
resources are applied. The migration walks every
`kuberneteses.apps.cozystack.io`
Application CR cluster-wide and renames `nodeGroups[*].ephemeralStorage`
to
`nodeGroups[*].diskSize`, preserving the user's value.

- Idempotent: a second run is a no-op (field is already absent)
- Best-effort: a failed patch is logged; the version stamp stays at 41
so the
  migration retries on the next platform upgrade
- Bumps `migrations.targetVersion` 41 → 42

The chart-side guard remains in place with an updated error message that
directs
operators to the migration job logs if the field somehow reappears
post-upgrade.

## Testing

Verified on dev cluster: migration correctly renames `ephemeralStorage`
values
in existing Application CRs. All 123 helm unit tests pass.

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

## Release Notes

* **Documentation**
* Platform migration 41 now automatically handles the `ephemeralStorage`
to `diskSize` field rename for Kubernetes node groups
* Migration is transparent during upgrade with no manual configuration
changes required
  * Error messages updated with clearer troubleshooting guidance

* **Tests**
  * Updated test assertions for migration error messages

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2688?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-21 16:37:26 +05:00
Myasnikov DaniilandIvanHunters 14e196cf22 fix(postgres): drive postgresql.parameters int-or-string from the generator
Ivan's original schema edits were correct in intent but lived in the
generated artifacts (values.schema.json and the embedded openAPISchema
in postgres-rd/cozyrds/postgres.yaml). `make generate` reverts them
because values.yaml's `@field {map[string]string}` annotation is the
source of truth -- same regeneration trap that caught #2466 (issue #2597).

cozyvalues-gen v1.4.0 (cozystack/cozyvalues-gen#25, merged 2026-05-21)
adds an `intOrString` type alias for map values that emits
`additionalProperties.anyOf: [integer, string]` plus
`x-kubernetes-int-or-string: true`. Move the change to source by
flipping the annotation, then regenerate:

- packages/apps/postgres/values.yaml: `@field {map[string]intOrString}`
  with updated description noting integer values are accepted.
- packages/apps/postgres/values.schema.json: regenerated -- now carries
  anyOf + x-kubernetes-int-or-string: true.
- packages/system/postgres-rd/cozyrds/postgres.yaml: embedded
  openAPISchema regenerated to match.
- packages/apps/postgres/README.md: regenerated type column.
- api/apps/v1alpha1/postgresql/types.go: Parameters is now
  `map[string]intstr.IntOrString`.

The result survives future regenerations. The template's
`{{ \$value | toString | quote }}` already coerces both inputs to
quoted strings, verified end-to-end with helm template against both
`max_connections: 100` (int) and `max_connections: "100"` (string) --
identical render output. Dangerous-param block list still rejects
archive_cleanup_command and recovery_end_command additions from the
prior commit.

Co-Authored-By: IvanHunters <xorokhotnikov@gmail.com>
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-21 16:04:07 +05:00
IvanHunters 388e823916 fix(postgres): block archive_cleanup_command and recovery_end_command
Add archive_cleanup_command and recovery_end_command to the dangerous
parameters blocklist in db.yaml. Both are PostgreSQL shell-exec hooks
identical in threat model to archive_command/restore_command and must
not be settable by tenants.

Also update the BLOCKED list in values.schema.json, values.yaml, and
the embedded openAPISchema in cozyrds/postgres.yaml to reflect the
two new blocked parameters.

Additionally update the embedded openAPISchema in cozyrds/postgres.yaml
to allow integer values for postgresql.parameters (anyOf string|integer),
matching the fix already applied to values.schema.json.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-05-21 16:04:07 +05:00
IvanHunters 8c89556e79 fix(postgres): accept integer values for postgresql.parameters in schema
The template already coerces all parameter values to strings via
'toString | quote', but the JSON schema had 'additionalProperties: {type: string}'
which caused Helm schema validation to fail before the template ran
when users passed integer values (e.g. max_connections: 100).

Accept both string and integer types in additionalProperties so that
existing configurations with unquoted numbers continue to work.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-05-21 16:04:07 +05:00
Myasnikov Daniil ed0297eb87 refactor(build): standardize image tagging to fix concurrent PR push conflicts
Every package Makefile tagged images via `settag` which, on a non-release
commit, collapsed to `:latest`. Concurrent PR builds therefore raced on every
image's `:latest` manifest in the OCIR registry. Two outliers (metallb,
redis-operator) used hardcoded version tags and collided across PRs even when
`settag` would not have.

Replace `settag` and the `TAG=latest` fallback with an `image-tags` macro
driven by three env vars:
  - IMAGE_TAG          — build-unique tag, always pushed
  - PUBLISH_VERSIONED  — also push :<component-version>
  - PUBLISH_FLOATING   — also push :latest

PR CI sets IMAGE_TAG=pr-<N>-<sha> with both PUBLISH_* off, so every push is
unique across runs. Release CI sets IMAGE_TAG=<git-tag> with both PUBLISH_*
on, preserving the previous release output and adding a uniform :<git-tag>
tag across every image. Local builds default to IMAGE_TAG=dev with PUSH=0
already off.

Image consumers reference digests pinned into values.yaml by each Makefile,
so the tag is decorative for cluster-side pulls; the build-unique tag is
sufficient for E2E.

Drops the redundant `:<component-version>-<git-tag>` form (cilium, linstor,
matchbox, ubuntu-container-disk, kubevirt-*, cluster-autoscaler) — nothing
in the repo or downstream values.yaml consumed it. The same information is
preserved via the new uniform `:<git-tag>` plus the existing
`:<component-version>` on release.

ubuntu-container-disk builds a separate image per Kubernetes version in a
loop. Switched its primary tag to `:<k8s-ver>-<IMAGE_TAG>` so loop
iterations within a single build do not push to the same tag.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-21 15:23:18 +05:00
myasnikovdaniil 243d20ea30 fix(cluster-api): fix kamaji OOM and set limits on unset providers (#2708)
## What this PR does

Fixes resource specs on CAPI provider pods in `cozy-cluster-api`.

**Kamaji control-plane-provider — fix OOMKilled crashloop.**
The override in
`packages/system/capi-providers-cpprovider/templates/providers.yaml`
targeted `containers[name=manager]`, but the upstream container in
`docker.io/clastix/cluster-api-control-plane-provider-kamaji` is named
`controller`. The CAPI operator merges container overrides by container
name, so the intended `cpu: 1 / memory: 1024Mi` limits were silently
dropped and the upstream default of `memory: 128Mi` was used. The kamaji
controller pod therefore restarts repeatedly with `exit code 137`
(OOMKilled), even at idle.

**Core / bootstrap / kubevirt providers — add limits.**
Upstream `*-components.yaml` for these three providers ships with no
`resources:` block, and cozystack added no override. The pods run
BestEffort and are first to evict under memory pressure. Set modest
limits (`500m / 256Mi`) and requests (`10m / 64Mi`) matching typical
CAPI controller-manager footprint.

### Release note

```release-note
fix(cluster-api): fix the resource override for the kamaji control-plane-provider (was silently ignored due to a wrong container name, causing OOMKilled restarts) and set requests/limits for the core, kubeadm-bootstrap, and kubevirt-infrastructure providers (were running BestEffort).
```

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

## Summary by CodeRabbit

* **Chores**
* Updated provider deployment configurations to establish resource
management parameters (CPU/memory requests and limits) for container
deployments across multiple provider types, improving scheduling
optimization and resource predictability
* Standardized container naming conventions in provider templates for
consistency

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2708?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-21 15:14:21 +05:00
Myasnikov Daniil 6ae66f72a6 fix(cluster-api): fix kamaji OOM and set limits on unset providers
The kamaji control-plane-provider override in templates/providers.yaml
targeted containers[name=manager], but the upstream container is named
"controller". The CAPI operator merges container overrides by name, so
the intended cpu: 1 / memory: 1024Mi limits were silently dropped and
the upstream default of memory: 128Mi was used, producing repeated
OOMKilled restarts of the kamaji controller (exit code 137).

The other three providers (core, bootstrap, infraprovider/kubevirt)
ship upstream with no resources block and have no cozystack override,
so they run BestEffort and are first to evict under memory pressure.
Set modest limits (500m / 256Mi) and requests (10m / 64Mi) matching
typical CAPI controller-manager footprint.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-21 12:57:02 +05:00
IvanHunters ed1bb53d2c fix(platform): migrate ephemeralStorage to diskSize via pre-upgrade hook
Add pre-upgrade migration 41 that walks all kuberneteses.apps.cozystack.io
Application CRs and renames nodeGroups[*].ephemeralStorage to diskSize,
preserving the user's value.

Without this migration, clusters upgraded after PR #2454 either fail to
reconcile (hard fail blocks all Flux operations) or silently lose the user's
disk size setting (default 20Gi replaces whatever was configured).

The migration is idempotent and best-effort: a failed patch is logged and
leaves the version stamp at 41 for retry on next upgrade.

Update the chart-side guard error message to direct operators to the
migration job logs when the field appears post-upgrade (regression detector).
Bump migrations.targetVersion 41 -> 42.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-21 12:14:55 +05:00
myasnikovdaniil 9c87996da8 fix(kafka): remove ZooKeeper PVCs on uninstall (#2679)
## What this PR does

Aligns ZooKeeper storage cleanup with the existing Kafka broker behavior
so deleting a Kafka release reclaims all of its persistent storage. The
Strimzi `Kafka` CR template already set `deleteClaim: true` on the
broker JBOD volume, but the ZooKeeper persistent-claim was left at
`deleteClaim: false`. As a result, removing a Kafka application freed
the broker PVCs while orphaning the ZooKeeper PVCs in the tenant
namespace, requiring manual cleanup before the same release name could
be reused.

The change is a single field flip in
`packages/apps/kafka/templates/kafka.yaml` on the
`spec.zookeeper.storage` block.

### Behavior change

Users who previously relied on orphaned ZooKeeper PVCs as an implicit
"soft delete" for ZooKeeper state recovery should be aware that
uninstalling a Kafka release now also removes the underlying ZooKeeper
storage. Take a backup before deletion if ZooKeeper data retention is
required.

### Screenshots

Not applicable — no UI changes.

### Release note

```release-note
fix(kafka): ZooKeeper PVCs are now reclaimed when a Kafka release is deleted, matching the existing Kafka broker storage behavior.
```


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

* **Chores**
* Kafka storage behavior updated so persistent volumes are automatically
removed when claims are deleted, changing storage lifecycle for
deployments.

* **Tests**
* End-to-end cleanup adjusted to remove the Kafka resource and verify
volumes are reclaimed automatically, adding waits to confirm PVC binding
and removal.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2679?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-21 11:37:55 +05:00
Andrei KvapilandClaude 1c059b3ba3 chore(platform): drop dashboard.cozystack.io CRDs via migration 40
Existing clusters still have the legacy dashboard.cozystack.io CRDs
installed even after the dashboard controller has been removed.
Add a platform migration that deletes them and bump targetVersion
to 41.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-05-20 16:33:21 +02:00
Andrei KvapilandClaude 4d5486cd84 chore(controller): remove dead dashboard controller
The dashboard controller used to project ApplicationDefinitions
into a set of dashboard.cozystack.io CRDs (Sidebar, Factory,
Navigation, Breadcrumb, CustomFormsOverride, CustomFormsPrefill,
CustomColumnsOverride, MarketplacePanel, TableUriMapping,
CFOMapping, BreadcrumbInside) consumed by the legacy UI.

cozystack-ui now reads ApplicationDefinitions directly, so the
controller, its API types, the generated CRDs and the related
RBAC rules are no longer used.

Removes:
- internal/controller/dashboard
- api/dashboard/v1alpha1
- packages/system/cozystack-controller/definitions/dashboard.cozystack.io_*.yaml
- dashboard.cozystack.io wiring from cmd/cozystack-controller/main.go
- dashboard.cozystack.io RBAC rules from cozystack-controller and cozy-dashboard charts
- ./api/dashboard/... codegen path from hack/update-codegen.sh

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-05-20 16:33:21 +02:00
Myasnikov Daniil e5437240e7 fix(opensearch-operator): use values.yaml override instead of patch
Move the kube-rbac-proxy image override from a patch + vendored-chart
edit to a values.yaml override under kubeRbacProxy.image.repository.

This matches the project convention documented in docs/agents/overview.md
("Override configuration in root values.yaml") and the pattern used by
sibling packages such as kamaji.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-20 15:45:08 +05:00
Myasnikov Daniil d0a0b06afd fix(opensearch-operator): replace deprecated kube-rbac-proxy image
The gcr.io/kubebuilder/kube-rbac-proxy image is no longer available
since GCR was deprecated. Replace it with quay.io/brancz/kube-rbac-proxy
from the original upstream author.

Applied as a local patch in patches/ because upstream chart 2.8.1+ drops
kubeRbacProxy entirely but also bumps CRDs to controller-gen v0.19.0 with
significant schema changes — out of scope for this fix.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-05-20 15:45:08 +05:00
Arsolitt 69a479df5a fix(kafka): reclaim ZooKeeper PVCs on Kafka release deletion
Set deleteClaim: true on the ZooKeeper persistent-claim storage so the
Strimzi operator removes the PVCs together with the rest of the cluster
when the Kafka CR is deleted. Previously the Kafka broker PVCs were
reclaimed (deleteClaim: true) but the ZooKeeper PVCs were orphaned,
forcing manual cleanup before the same release name could be reused.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-05-20 13:45:07 +03:00