mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 14:02:56 +09:00
8f4c19daaeb00a2aeccf761cd96879867a4d86ce
5128
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
19634fd45d |
fix(kafka): mark tls.enabled field type as *bool in values.yaml
The README is regenerated from the @field annotation, so declaring the field as bool produced a README default of false that disagreed with the schema's actual ["boolean", "null"] tri-state. Switching the annotation to *bool propagates a *bool/null entry into the generated README. Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
284c5dc985 |
fix(kafka): treat tls.enabled=null as unset in Kafka CR template
When tls.enabled is present in values but set to null, the previous logic treated it as explicit (hasKey returned true), assigned null to $tlsEnabled, and rendered tls: null on the external listener — an invalid Strimzi CR field value. Introduce $tlsEnabledRaw and guard the explicit check with an additional ne nil condition so that null behaves like an absent key and falls back to inheriting from .Values.external. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
d1ec6e477c |
fix(kafka): gate dashboard RBAC external-bootstrap solely on external flag
The dashboard-resourcemap template was using an OR condition that also enabled the external-bootstrap Role entry when tls.enabled=true with external=false. This violates least-privilege: the external listener is only created when external=true (matching kafka.yaml), so RBAC for it must follow the same gate. Simplify $showExternal to .Values.external | default false and update the test to assert the external-bootstrap entry is absent when external=false, regardless of tls.enabled. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
cbd9818d2d |
docs(kafka): correct tls.enabled default to null in README
The field is tri-state (null/true/false): null means inherit from external. Showing false as the default was misleading. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
46494307ca |
build(kafka): assert *bool patch applied in Makefile generate target
Add a hard assertion after the sed substitution so the build fails immediately if cozyvalues-gen output format changes and the patch silently no-ops, instead of letting an incorrect non-pointer type slip through. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
615415d676 |
test(kafka): add cluster-ca-cert checks to kafka-rd secrets bats test
Mirror the existing clients-ca-cert presence/rejection tests for cluster-ca-cert so the suite enforces both Strimzi-managed CA secret names and rejects bare cluster-ca references. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
377e7702ca |
fix(kafka): use printf in kafka-rd-schema bats test
dash's echo interprets backslash escapes by default (unlike bash), which mangles double-backslash sequences in the embedded JSON pattern strings and makes jq report an invalid-escape parse error. printf '%s' passes the value through untouched. Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
ddc3b5204b |
fix(kafka): replace bash-ism in kafka-rd-schema bats test
Replace double-bracket [[ ... ]] with POSIX [ ... ] in hack/check-kafka-rd-schema.bats so it runs under dash, which is what /bin/sh resolves to on the Ubuntu CI runners. The test passed locally because /bin/sh resolves to bash on most developer machines. Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
80a01fd330 |
chore(kafka): regenerate codegen deepcopy artifacts
Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
f37149c70d |
fix(kafka): decouple external listener from tls.enabled (tri-state)
The external LoadBalancer listener (port 9094) was rendered whenever tls.enabled was explicitly true, regardless of the external flag. This caused Strimzi to stall waiting for a LoadBalancer IP when tls.enabled=true but external=false. External is now the sole gate for the listener. tls.enabled only controls TLS on the listener when it is rendered (auto-on when external=true and tls.enabled is unset, explicit value otherwise). Update tests to match. Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
6f15714846 |
test(kafka): add TLS test coverage
- Add helm-unittest suite covering TLS listener config, tri-state nil/true/false cases - Cover external=false + tls.enabled=false negative combinations - Add dashboard resourcemap tests for CA secret inclusion - Add bats tests for cozyrds schema nullable field and Strimzi CA secret naming Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
9407ccabd9 |
feat(kafka): implement TLS via Strimzi listener configuration
- Configure Kafka CR with TLS listener spec using Strimzi-managed PKI - Gate external bootstrap RBAC resources on $showExternal predicate - Use resolved $tlsEnabled variable for consistent tri-state evaluation - Add Strimzi clients CA secret reference to dashboard resourcemap Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
2eba34618c |
feat(kafka): add TLS schema and generated artifacts
- Add tri-state tls.enabled (*bool) to Kafka API types for strict nil/true/false semantics - Extend values.yaml and JSON schema with TLS configuration block - Update Makefile with sed+jq patching for nullable bool schema and test target - Update cozyrds schema to reflect nullable tls.enabled type - Update README with Strimzi-managed PKI documentation and authentication notes Signed-off-by: Arsolitt <arsolitt@gmail.com> |
||
|
|
b630706df6 |
feat(apps): mark stateful-app storageClass fields as immutable (#2639)
## Summary Annotate `storageClass` as immutable across every stateful chart that exposes one: clickhouse, foundationdb, harbor, http-cache, kafka (Kafka + ZooKeeper), kubernetes (top-level + nodeGroups[]), mariadb, mongodb, nats, openbao, opensearch, postgres, qdrant, rabbitmq, redis, vm-disk. The PVC-pinning argument is identical for every chart — Kubernetes fixes `storageClassName` on each PVC at creation, so editing this field on an existing resource never migrated data; the schema annotation makes that contract explicit and lets schema-aware consumers refuse the edit up-front. Schema-aware consumers today: the Cozystack UI (cozystack/cozystack-ui#6) reads the rule from the chart's openAPISchema and renders the field read-only on edit forms. This PR is **schema-side only**. The cozystack aggregated apiserver does not yet evaluate CEL rules in `openAPISchema` (`MariaDB` / `Postgres` / etc. are application-backed, not CRDs); tracking issue cozystack/cozystack#2657 covers wiring CEL into `pkg/registry/apps/application/rest.go::Update`. Until that lands, an apiserver-level `kubectl patch` that changes `storageClass` is still accepted — but the underlying StatefulSet PVCs do not move, so there's no data-corruption risk, only a confusing UX. The UI catches that case today; the apiserver gate closes it permanently. ## What changes For each app in the list above: - `packages/apps/<app>/values.yaml` — gain a `## @immutable` directive directly above (or below, for fields inside a `@typedef`) the `storageClass` line. - Regenerated via `make generate` in each package: - `api/apps/v1alpha1/<app>/types.go` — kubebuilder `XValidation` marker on the relevant field(s). - `packages/apps/<app>/values.schema.json` — `x-kubernetes-validations: [{rule: "self == oldSelf", message: …}]` on `storageClass`. - `packages/apps/<app>/README.md` — regenerated parameter table. - `packages/system/<app>-rd/cozyrds/<app>.yaml` — the embedded `openAPISchema` on the ApplicationDefinition reflects the change. Refactor: `pkg/registry/apps/application/rest.go` factors the inline schema-build path into a new `buildSpecSchema` helper so it can be unit-tested without standing up a fake apiserver. Three regression tests in `pkg/registry/apps/application/rest_spec_schema_test.go` pin the contract that `x-kubernetes-validations` survives the v1→internal→structural conversion (test loads the actual MariaDB ApplicationDefinition fixture on disk, asserts the rule reaches the structural schema). Empty-input and malformed-JSON paths are also pinned. No e2e admission test is added because the apiserver does not yet enforce the rule — see cozystack/cozystack#2657. No changelog entry: schema-side annotation is invisible at the apiserver layer today (UI-only consumer) and the release-cut PR will add the entry when admission enforcement lands. ## Dependencies Requires cozystack/cozyvalues-gen#24 to be merged and released, and the `cozyvalues-gen` binary pin in `.github/workflows/pre-commit.yml` line 50 bumped to the release that includes that PR. Pre-commit CI will fail until both happen — the stock `v1.3.0` binary silently ignores `## @immutable` and regenerates artefacts without the rule, producing a drift against what's committed. Once that ordering is satisfied, rebase this branch and the pre-commit job will be green. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * storageClass is now treated as immutable after creation for many stateful apps (enforced by chart/CRD schema validation). * **Documentation** * Added a storage immutability guide explaining the contract and enforcement. * Updated chart READMEs and values docs to mark storageClass as immutable and clarified nodeGroup storage remains intentionally mutable. * **Tests** * Added tests validating schema parsing and preservation of immutability rules. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2639?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 --> |
||
|
|
d3f282cee1 |
test(e2e): pre-pull platform images via staged-busybox DaemonSet (#2724)
## What this PR does Pre-pulls every image referenced by `packages/system/kubeovn`, `packages/system/linstor`, and `packages/system/cert-manager` onto every node before those HelmReleases install. Cluster-member workloads (OVN raft, LINSTOR, cert-manager webhook) fail when replicas start at different times due to per-node image-pull stagger; pre-pulling means all replicas start with images already cached. ## Mechanics Renders each chart with `helm template`, then runs a `yq` filter that walks every PodSpec-shaped object and emits the images of each container — scoping the output to images the kubelet actually pulls (skips configmap fields and CRD examples that happen to contain an `image:` key). Each render is staged through a tmp file so a helm-template failure trips `set -e` cleanly (cozytest.sh runs `@test` bodies under `/bin/sh`, which is dash on Ubuntu CI — no `pipefail`). The pre-pull DaemonSet itself is the hard part: distroless images (cert-manager and friends) ship no shell and no `/bin/sleep`, so the obvious `command: ["sleep", "infinity"]` fails with `exec: ENOENT`. The script stages a statically-linked `busybox:musl` into a shared `emptyDir` from an initContainer; every prepull container then execs `/shared/sleep` regardless of what the image itself ships. The glibc `busybox` tag is unusable — it needs `/lib64/ld-linux-x86-64.so.2` which distroless lacks. ## Origin Lifted from #2619 — squash of `test(e2e): pre-pull cert-manager images` → POSIX rewrite → cert-manager removal (failed on distroless) → reintroduction with the busybox helper that solves the distroless problem. Final state only — the four iterative commits aren't review-friendly individually. ## Verification - `sh -n hack/e2e-prepull-images.sh` clean - All chart-referenced images held Ready under the DaemonSet on local kind v1.33.1 single-node ### Release note ```release-note NONE ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Improved end-to-end test reliability for platform image pre-pulling by rendering charts separately and consolidating extracted images. * Simplified and consolidated readiness waits during tenant configuration for more robust application readiness checks. * **Chores** * Enhanced image pre-pulling to reliably handle distroless images using a staged helper binary and shared volume approach. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2724?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 --> |
||
|
|
5af5962edf |
feat(apps): mark stateful-app storageClass fields as immutable
Annotate storageClass as immutable across every stateful app whose chart exposes one — clickhouse, foundationdb, harbor, http-cache, kafka (Kafka + ZooKeeper), kubernetes (top-level only), mariadb, mongodb, nats, openbao, opensearch, postgres, qdrant, rabbitmq, redis, vm-disk. Without coverage across the catalogue the UI would render storageClass read-only on some apps and editable on others — a worse mental model than uniform behaviour. The annotation lands at the schema layer (CEL XValidation rule 'self == oldSelf' on values.schema.json / types.go / embedded openAPISchema in cozyrds/<app>.yaml). The cozystack aggregated apiserver does not yet evaluate CEL on application-backed resources; that is tracked at cozystack/cozystack#2657. The UI (cozystack/cozystack-ui#6) consumes the marker today. The PVC-pinning argument is identical for every chart: Kubernetes fixes a PVC's storageClassName at creation time, so editing this field on an existing resource never migrated data. The schema annotation makes that contract explicit. kubernetes.nodeGroups[name].storageClass is intentionally NOT annotated: the field is optional and undefaulted, so a strict 'self == oldSelf' rule would block any future attempt to set it on an existing node group. A richer ratcheting form is needed there — see docs/storage-immutability.md. A refactor in pkg/registry/apps/application/rest.go extracts the inline schema-build path into buildSpecSchema so it can be unit-tested. Three regression tests in rest_spec_schema_test.go pin the contract that x-kubernetes-validations survives the v1->internal->structural conversion (loaded from the actual MariaDB ApplicationDefinition fixture on disk, not a hand-edited mock). Each per-app README gains a one-line blockquote pointing to a new docs/storage-immutability.md that explains the contract and which consumers enforce it today. Requires cozystack/cozyvalues-gen with @immutable support — bump the pre-commit pin in .github/workflows/pre-commit.yml once the release exists. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
f85ec76e19 |
feat(hack): rewrite check-readiness as a Go command with test suite
Replace hack/check-readiness.sh with a Go command at cmd/check-readiness, wired into the build like cozypkg (`make check-readiness`, Version via -ldflags). The tool shells out to kubectl and reports only non-ready cozystack / Flux / Kubernetes resources parsed from status conditions, supporting --wait/--timeout, --watch, --core, --parallel, --verbose, -n/-l scoping, --no-color and --version. Add a black-box golden test suite under test/check-readiness driven by a mock kubectl over fixtures; `make test-check-readiness` runs it and it is wired into `make unit-tests`. Supersedes #2740, which added the same feature set to the bash script. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
12ac2adec9 |
chore: retrigger CI
Empty commit to fire the Pull Request workflow, which never ran for this commit because GitHub Actions was down during the original push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
a58c0d193b |
chore: retrigger CI
Empty commit to fire the Pull Request workflow, which never ran for 3bac5cac because GitHub Actions was down during the original push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
7e92265823 |
chore(img): refresh Cozystack logo and add icon/stacked variants (#2746)
## Summary
- Replaces `img/cozystack-logo-black.svg` and
`img/cozystack-logo-white.svg` with the updated Cozystack brand mark
(icon + "COZYSTACK" wordmark).
- Adds new layouts so the logo can be used on more surfaces without
re-cropping:
- `cozystack-logo-stacked-{black,white}.svg` — icon stacked over the
wordmark
- `cozystack-icon-{black,white}.svg` — icon-only mark
- All SVGs are exported on a transparent background, so the same file
works on light and dark surfaces.
## Files
| Path | State | Notes |
|---|---|---|
| `img/cozystack-logo-black.svg` | replaced | horizontal wordmark, black
fill, transparent bg |
| `img/cozystack-logo-white.svg` | replaced | horizontal wordmark, white
fill, transparent bg |
| `img/cozystack-logo-stacked-black.svg` | new | icon over wordmark,
black fill, transparent bg |
| `img/cozystack-logo-stacked-white.svg` | new | icon over wordmark,
white fill, transparent bg |
| `img/cozystack-icon-black.svg` | new | icon only, black fill,
transparent bg |
| `img/cozystack-icon-white.svg` | new | icon only, white fill,
transparent bg |
## Test plan
- [ ] Verify rendering of `cozystack-logo-{black,white}.svg` in the
README on both light and dark GitHub themes.
- [ ] Confirm no other in-repo references to `img/cozystack-logo-*.svg`
need width/height adjustments (existing aspect ratio ~2.8:1 → new ~3:1,
very similar).
|
||
|
|
6451373251 |
chore(img): refresh Cozystack logo and add icon/stacked variants
Replace the old "COZYΞTACK" wordmark with the updated brand mark
(icon + "COZYSTACK" wordmark) and add additional layouts:
- cozystack-logo-{black,white}.svg horizontal wordmark (replaced)
- cozystack-logo-stacked-{black,white}.svg icon over wordmark (new)
- cozystack-icon-{black,white}.svg icon only (new)
All variants use a transparent background so they work on both light and
dark surfaces. SVGs are optimized with https://autocrop.cncf.io while
keeping the original padding.
Signed-off-by: Viktoriia Kvapil <bisunviktoria@gmail.com>
|
||
|
|
2abc7c2ab0 |
fix(dashboard): unblock token-proxy startup when JWKS is briefly unreachable
token-proxy panicked at startup whenever the very first JWKS fetch against kubernetes.default.svc failed, which is routine on a fresh install: CoreDNS, the CNI, or the API-server endpoint may not be ready in the seconds between the pod scheduling and the cache initialization running. The pod then entered CrashLoopBackOff with exit code 2 and no logs, the dashboard HelmRelease never installed, and every release that depended on it stalled behind it. Two paths into init() touched the network synchronously: jwk.Cache.Register with the default WaitReady(true), and an explicit follow-up Refresh. WaitReady has no built-in timeout, so a permanently-unreachable JWKS would turn the panic into a different failure mode (an unbounded startup wait) rather than fixing it. The explicit Refresh on top of Register was redundant. Drop the redundant Refresh and pass WithWaitReady(false) on Register so the cache setup returns immediately. The background worker keeps retrying: a pure network failure (DNS, dial, TLS) skips Sync's SetNext and falls back to httprc's 1-second tick, while an HTTP error response (apiserver 5xx, RBAC 403) honors MinInterval — lower it from 5 minutes to 5 seconds so a single bad response can no longer hold the pod NotReady for several minutes. Move the cache construction out of init() into a testable function; init() now only registers flags. Background fetch failures are wired into the container log via httprc.WithErrorSink so a persistent JWKS misconfig produces a recognisable stdout signature instead of being invisible behind the readiness probe. To prevent the new failure mode (silently broken sign-in when JWKS is permanently misconfigured) from regressing observability, expose /healthz/live and /healthz/ready: liveness is process-up; readiness calls jwkCache.Lookup so a permanent misconfig surfaces as 0/1 Ready and keeps the pod out of service endpoints. A startupProbe pointed at /healthz/ready with a generous failure budget (~5 minutes) serves as a backstop: a genuinely stuck pod eventually restarts so the operator sees RestartCount/CrashLoopBackOff instead of a silently NotReady pod that nothing escalates. The oauth2-proxy branch keeps its own /ping probes unchanged. The sign-in handler distinguishes errJWKSNotReady from a real invalid token: during the cold-start window users see "Service is starting, please retry in a moment" instead of the misleading "Invalid token". Tests cover: WaitReady(false) contract (Register returns in <1s against unreachable JWKS), background-worker recovery from transient HTTP 5xx without an explicit Refresh (the production cold-start path), lazy population, catastrophic-misconfig surface (missing/malformed CA still returns hard error), errJWKSNotReady disambiguation, SA token injection, and /healthz/ready 503-then-200 transition. The package Makefile gains a test-go target wired into the existing 'test' target so hack/helm-unit-tests.sh picks up the new Go tests automatically. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
727343d259 |
docs(backups): add NATS Job-strategy example
Add examples/backups/nats/, the first worked example of the generic Job
backup strategy. It backs up a NATS JetStream stream that has no dedicated
driver, using a stock natsio/nats-box image: 'nats stream backup' -> tar ->
curl --aws-sigv4 PUT to the COSI/seaweedfs S3 endpoint, and the reverse for
restore. One PodTemplateSpec serves both directions, branching at runtime on
{{ .Mode }} (the engine templates each string leaf independently and cannot
restructure the Pod per mode).
Mirrors the clickhouse example's structure: numbered scripts (bucket -> NATS
src + seeded stream -> strategy -> BackupClass -> BackupJob -> in-place and
to-copy RestoreJob), shared helpers, run-all.sh, README, and scenario docs.
Like every examples/backups/* dir it is a runnable demo, not CI-gated.
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
|
||
|
|
5029af6d83 |
test(backups): harden Job strategy tests
Address review feedback on the Job backup strategy: - Assert .Application is templated: the rendered backup Job now reads .Application.spec.replicas, the load-bearing context key that lets a generic backup tool read live app spec (previously only .Release, .Mode, .Parameters and .Backup had coverage). - Pin failure-path messages so a reordered error path can't pass on the wrong branch: the missing-strategy test asserts the strategy name, and the unmappable-kind tests assert "kind not registered". - Document why the full Status().Update in the success branches is safe (the object is freshly Get() each pass; the StartedAt write requeues). - Note the fixed batch/v1.Job backoffLimit=2 and link the follow-up to make it configurable (#2742). Signed-off-by: Timofei Larkin <lllamnyp@gmail.com> |
||
|
|
adddd4f6e1 |
feat(dashboard): add cluster-usage RBAC for the new admin page
The cluster-usage admin page in the dashboard reads cluster-scoped resources (nodes, pods, metrics.k8s.io) to render aggregate utilisation and a per-node table that includes arbitrary extended resources such as GPUs. The built-in 'edit' ClusterRole already bound to cozystack-cluster-admin via cozystack-dashboard-cluster-admin does not grant cluster-wide pod read or any access to 'nodes', so the dashboard needs an explicit grant for the page to render anything beyond an error. The page-side sidebar item is gated by SelfSubjectAccessReview on 'nodes list' — without this binding the gate fails closed and the entry stays invisible, which is the desired fallback for clusters that have not adopted the new RBAC yet. The chart intentionally does not add permissions for deeper sources (Prometheus, DCGM, HAMi). Those belong in their respective packages or operator-provided RBAC; the page detects them via discovery and gates each section with its own per-source review. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
3bac5caac1 |
fix(platform): remove duplicate test target in Makefile
The deletion-protection commit added a second, identical 'test:' recipe to the platform Makefile, causing GNU Make to print 'overriding recipe' warnings on every 'make test' (and thus in helm-unit-tests CI output). Drop the duplicate; keep the original target. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com> |
||
|
|
2ace581117 |
test(e2e): stage prepull image list to avoid yq pipefail trap
Without pipefail (dash-CI), `yq | hack/e2e-prepull-images.sh` would let a yq failure produce empty input; prepull-images.sh exits 0 on no-input and the test passes with pre-pull silently skipped. Stage the yq output to a tmp file and feed it via stdin so set -e trips on a yq failure. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
2fa15ce044 |
fix: startupProbes for kubevirt-cdi-operator and grafana-operator (#2725)
## What this PR does Adds `startupProbe` to two operator deployments whose readiness probes fail repeatedly during a cold cluster install, generating noise and (for CDI) blocking dependants on slow leader-election: - **`kubevirt-cdi-operator`** — leader-election against a slow `kube-apiserver` (Kamaji control plane bringup) consistently takes >30s on a cold install. The default readinessProbe with 1s `periodSeconds` / 3 `failureThreshold` fires the first time election times out, the operator pod gets marked NotReady, and dependants block on it. - **`grafana-operator`** — bootstrap CRD discovery + Helm-managed Dashboard reconcile loop spikes startup duration past readinessProbe's default budget under load. `startupProbe` gives each operator a longer one-time window to reach steady state; the regular `readinessProbe` then takes over with its tight per-second cadence to detect genuine ongoing failures. ## Origin Both commits lifted unchanged from #2619. ### Release note ```release-note NONE ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Configuration Updates** * Added startup health checks to Grafana operator container * Added startup health checks to KubeVirt CDI operator container <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2725?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 --> |
||
|
|
edb3eeb011 |
fix(info): use root-host for Keycloak OIDC issuer URL in tenant kubeconfig (#2704)
## What this PR does The kubeconfig generated by the `info` package for non-root tenants pointed `--oidc-issuer-url` at `keycloak.<TENANT>.<ROOT-DOMAIN>`, but Keycloak's ingress and cert live at `keycloak.<ROOT-DOMAIN>` (built from `_cluster.root-host`). `kubectl oidc-login` then failed TLS verification against the nginx-ingress default fake cert, so the auto-generated kubeconfig was unusable for any non-root tenant. `$host` was being sourced from `_namespace.host` (tenant-specific subdomain) with a lookup-based override that tried to read `tenant-root`'s `spec.values.host`. That override has been dead since #1787 migrated `tenant-root` to `valuesFrom` (Secret-backed) — `spec.values` is now always empty, so the override never fires. Source `$host` from `_cluster.root-host` directly, matching what `packages/system/dashboard/templates/gatekeeper.yaml` already does for the same Keycloak realm. Verified with `helm template tenant1 packages/extra/info -n tenant-tenant1 --set-string _namespace.host=tenant1.example.org --set-string _cluster.root-host=example.org ...` — issuer URL is now `https://keycloak.example.org/realms/cozy` regardless of the per-namespace host. ### Release note ```release-note fix(info): use root-host for the Keycloak OIDC issuer URL in tenant kubeconfigs so `kubectl oidc-login` succeeds against the real Keycloak cert on non-root tenants ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated kubeconfig generation to consolidate host configuration handling, now using a single authoritative source instead of multiple fallback mechanisms. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/cozystack/cozystack/pull/2704?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 --> |
||
|
|
76fd5d1791 |
fix(platform): address lllamnyp review on PR #2650
unprotect.sh: add `gitrepository` to the namespaced-kind list so the
platform stays unprotectable when an operator configures
`sourceRef.kind: GitRepository`. Currently only `ocirepository` is
swept, while repository.yaml templates `kind: {{ $sourceRef.kind }}`
unrestricted.
run-migrations.sh: replace the silent skip on a missing migration file
with a hard error. All migrations 1..targetVersion exist in the shipped
image — a gap can only mean a packaging mistake (digest advanced
without the file landing), which previously stalled the cluster at a
stale version while reporting success.
migrations/42: drop the misleading "no 3-way-merge subtlety" claim from
the header. The apply records the no-delete label under the default
field manager's last-applied annotation, so a future label-less apply
by the same field manager would strip it. Document the constraint
instead of denying it.
hack/e2e-install-cozystack.bats: restore `helm.sh/resource-policy: keep`
on the reconstructed cozystack-version ConfigMap in the bypass-path
teardown so cluster state matches the chart template shape
(packages/core/platform/templates/cozystack-version.yaml) on exit.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
|
||
|
|
2bed6eb090 |
fix(platform): do not swallow kubectl errors in unprotect.sh
The blanket `2>/dev/null || true` suppression masked RBAC, wrong-context, and API-unreachable failures, letting the script print "Done." while leaving labels in place. Replace with a helper that only tolerates the "unknown resource type" error (the actual best-effort case — kinds that don't exist on every cluster, e.g. linstorcluster on a non-LINSTOR install) and propagates everything else. Also keep stdout (resource list) and stderr (kubectl warnings) separate so deprecation warnings can't sneak into the loop variable. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
c2f1bb2eac |
feat(platform): make unprotect strips no-delete label for teardown
Address review feedback from @lexfrei on packages/core/platform/templates/ deletion-protection.yaml: with this PR every protected object — CRDs, ClusterIssuers, cozy-system and tenant-root Namespaces, cozystack-version ConfigMap, cozystack-packages Repository, tenant-root HelmRelease, LinstorCluster, plus the VAP and Binding themselves — carries the no-delete label. `helm uninstall cozy-platform` issues DELETE on each, and DELETE is denied for every one of them, so the chart effectively cannot be uninstalled without manual `kubectl label` against each object. Disaster recovery and dev-cluster reset both regress as a result. Add packages/core/platform/hack/unprotect.sh: enumerates the guarded set locally (per-kind, not via a single all-resources sweep — some kinds may not exist on every install, e.g. linstorcluster on a non-LINSTOR cluster, and kubectl errors on unknown kinds). For cluster-scoped kinds, lists objects with the label and labels them off one at a time. For namespaced kinds, lists (namespace, name) pairs via jsonpath and labels each in the right namespace. Idempotent: a label that is already absent is a no-op. Wire `make unprotect` in packages/core/platform/Makefile so the operator runs it before `helm uninstall`. The Makefile target carries a comment noting this is teardown / disaster-recovery only — after it runs, the cluster has no guardrail until the next `helm upgrade` re-stamps the label. Verified with a stub kubectl in PATH that the script issues the right get/label calls for both cluster-scoped (e.g. `kubectl label customresourcedefinition/packages.cozystack.io platform.cozystack.io/ no-delete-`) and namespaced kinds (e.g. `kubectl label --namespace tenant-root helmrelease tenant-root platform.cozystack.io/no-delete-`). The template-header reference at templates/deletion-protection.yaml that forward-links to `make unprotect` was added in the preceding L3 commit; the recipe it points at now exists. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
155c011dd5 |
test(platform): e2e check that no-delete VAP denies + bypass works
Address review feedback from @lllamnyp on packages/core/platform/templates/
deletion-protection.yaml: the VAP contract had no integration coverage.
A unit test that greps the rendered manifest is the wrong shape — the
contract is "the api-server denies a labeled DELETE with the documented
message" and that needs a real apiserver in the loop.
Add a post-install bats test to hack/e2e-install-cozystack.bats that runs
once cozystack is installed on the e2e cluster:
1. Skips on clusters that pre-date Kubernetes 1.30 (no VAP API).
2. Asserts the cozystack-version ConfigMap actually carries
platform.cozystack.io/no-delete=true — precondition, so the deny
assertion below would not misreport on a regressed binding.
3. Asserts `kubectl delete configmap cozystack-version -n cozy-system`
exits non-zero AND its error message contains the documented
"Deletion blocked: ... platform.cozystack.io/no-delete=true"
substring AND the `--namespace` bypass hint.
4. Confirms the ConfigMap survives the rejected delete.
5. Removes the label, deletes (must succeed), recreates the ConfigMap
with the original data.version and the label so the cluster ends
the test in the same state it started.
This single test catches every regression the PR is meant to prevent:
capability gate inverted, binding objectSelector mistyped,
validationActions flipped Deny→Warn, expression flipped false→true,
label-key drift between the binding and the manifests, and the bypass
docs missing --namespace.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
5d263a1e25 |
test(crdinstall): assert no-delete label survives Install + merge
Address review feedback from @lllamnyp on internal/crdinstall/install.go: the deletion-protection label loop was untested — TestInstall_appliesAllCRDs only checked that Install returned no error, so a refactor dropping the loop would have shipped green and silently disabled VAP coverage on every Cozystack CRD. - Extend TestInstall_appliesAllCRDs to read each applied CRD back from the fake client and assert platform.cozystack.io/no-delete=true is present. - Add TestInstall_preservesExistingLabels: feeds in a CRD whose manifest already carries app.kubernetes.io/managed-by=foo, asserts both that label and the deletion-protection label end up on the applied object. Locks down the merge contract in install.go:92-101 (the GetLabels()==nil branch is still covered by the first test). Verified the new assertions fail when the label-stamping loop is removed. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 --> [](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 --> |
||
|
|
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
|
||
|
|
c85e473523 |
test(e2e): inheritance attach asserts controller writes, not Cilium Accepted
The previous e2e check waited for `HTTPRoute.status.parents[0].conditions[Accepted]=True` after applying a child tenant's HTTPRoute against the parent's Gateway. That condition is set by Cilium after the listener's TLS Secret becomes Ready — and in the e2e cluster every fresh certificate fails to issue (LE prod refuses every `.example.org` hostname by policy, visible across alerta/grafana/dashboard/seaweedfs-s3 cluster certs in cozyreport). So Cilium never set Accepted, the route timed out 3/3 retries, and the failure was environmental — not a regression in the inheritance code path. The contract the test exists to pin is cozystack-controller's behaviour, which has two observable writes: 1. The parent Gateway grows a per-listener HTTPS entry for the child route's hostname (proves collectHostnameClaims saw the route through the inheritance label and reconcileGateway appended the listener). 2. A per-listener Certificate object appears in the parent namespace with the route's hostname in spec.dnsNames (proves reconcilePerListenerCertificates ran for the same hostname). Both writes are deterministic and depend only on the inheritance label + the HTTPRoute existing — not on ACME, not on Cilium TLS readiness. The test now polls those two objects directly within shorter, scenario-appropriate timeouts (120s for the listener, 60s for the Certificate). The route Accepted check is dropped with an explanation in the test comment so a future cluster with a working ACME doesn't get a misleading pre-condition added back. The namespace inheritance label is also verified up front as the precondition collectHostnameClaims's filter reads. Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
6d8634e3ef |
docs(release): expand release.md into release-engineer playbook
Adds operational sections under the existing process-design walkthrough: CI phase-by-phase behavior, backport bot mechanics + cherry-pick triage, monotonic-counter migration trap, changelog failure modes + verification, three release-time war stories, pre-release verification checklist, force-retagging discussion + RFC link, bundle-PR splitting strategy. Mermaid diagrams and numbered RC/regular/patch walkthroughs are kept verbatim. Example versions normalized to simple semver-style (v1.2.0, v1.2.1, release-1.2) consistent across the doc. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
320172137e |
fix(tenantgateway): include inheriting child namespaces in hostname claim collection
The HTTP-01 inheritance flow deadlocked at the route-Accepted step:
the apps/tenant chart correctly labelled the child namespace with
`namespace.cozystack.io/gateway=<owner>`, the parent Gateway's
allowedRoutes selector matched by label, but `collectHostnameClaims`
filtered routes by the static set `{tgw.Namespace} ∪ Spec.AttachedNamespaces`
— so a child route in a namespace reached purely via the inheritance
label was silently dropped. No per-listener Certificate, no per-app
HTTPS listener, no hostname match → `Accepted=False,
Reason=NoMatchingListenerHostname` indefinitely.
Caught by the new `child tenant's HTTPRoute attaches to parent's
Gateway via inheritance label` e2e bats — failed 3/3 retries on the
CI run for `afc7765e6`. The Go controller-level
TestReconcile_HTTP01CollectsHostnamesFromInheritingChildNamespaces
test pins the same contract at a faster feedback loop: an HTTPRoute
in a namespace labelled `namespace.cozystack.io/gateway=<owner>` but
NOT listed in Spec.AttachedNamespaces must still cause the
controller to render a per-listener HTTPS listener for its hostname.
The fix extends the `allowed` set in `collectHostnameClaims` to
also include namespaces matched by the gateway label selector, so the
hostname-claim collector and the Gateway's allowedRoutes selector
agree on which namespaces can attach. DNS-01 mode is unaffected — it
returns nil from `collectHostnameClaims` immediately and uses the
separate `collectInheritingChildApexes` path for SAN expansion.
Signed-off-by: Aleksei Sviridkin <f@lex.la>
|
||
|
|
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 --> [](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 --> |
||
|
|
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 (
|