mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-30 08:37:31 +09:00
a75919087eeddd0bcabb501825df077dfd7d324e
6564
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a75919087e |
fix(opensearch-operator): bound every call to a cluster's HTTP API (#4341)
<!-- Thank you for making a contribution! Here are some tips for you: - Use Conventional Commits for the PR title: `type(scope): description` - Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore - Scopes are not an exhaustive list — pick the most specific scope for the change and extend the list when a genuinely new area appears. Examples: - System components: dashboard, platform, operator, cilium, kube-ovn, linstor, fluxcd, cluster-api - Managed apps: postgres, mariadb, redis, kafka, clickhouse, virtual-machine, kubernetes - Development and maintenance: api, hack, tests, ci, docs, maintenance - Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or add a `BREAKING CHANGE:` footer - If it's a work in progress, consider creating this PR as a draft. - Don't hesistate to ask for opinion and review in the community chats, even if it's still a draft. - Add the label `kind/backport` if it's a bugfix that needs to be backported to a previous version. --> ## What this PR does A call to a cluster's HTTP API has no bound of its own. `NewOsClusterClient` builds its own `http.Transport`, and a fresh transport carries no dial, TLS-handshake or response-header timeout; the config sets no `http.Client`, so there is no timeout there either; the calls the client makes on its own behalf pass `context.Background()`; and the calls that take a context from the caller are handed one with no deadline on it, since nothing on the reconcile path sets one. The `Timeout` field the request types carry is the server-side query parameter: it bounds how long the server waits before answering, which helps only once something answers. Each controller runs a single worker, so one call that never returns stops every `OpenSearchCluster` from being reconciled, and the finalizer on the next deletion waits out its whole termination timeout with nothing in the log to say why. Reported in #4337, measured in #4332. A fourth patch on the image bounds every request the operator's client sends, at two levels. - Every request goes out through a `RoundTripper` that puts a 30s deadline on it. That is the only level that reaches the request opensearch-go builds for itself: before a client's first request the library runs a product check with a `context.Background()` of its own and decodes the body of the answer, and a client is built per reconcile, so that check runs on every one. The deadline cannot be released when `RoundTrip` returns, because the body goes back to the caller still open, so the cancel rides on the body and is released by `Close`, which is how `http.Client` applies its own `Timeout`. - That bounds one attempt. opensearch-go retries a request up to three more times on 502, 503 and 504, and the transport's deadline starts over on each of them, so the calls carry a 30s deadline a second time for the series: the 15 calls in `os_client.go` that request and decode before returning through `requestContext`, and the 28 that go through the `doHTTP*` helpers in `performRequest`. - The transport keeps a 10s dial and a 10s TLS handshake, so an unreachable node fails in a third of the budget with an error that names what went wrong. `ResponseHeaderTimeout` is gone from the transport. It stops counting the moment the headers arrive, so it is no bound on a body that starts and then stops, and at 60s it sat above a request deadline that now covers everything it covered. This bounds the wait, it does not make an unresponsive cluster answer: a call that passes its deadline fails the reconcile, which requeues. The change belongs upstream, and there is no upstream issue for it yet. The Dockerfile entry carries the drop condition the other entries carry. ### Tests Seven, in the package the image's own `go test -race` step already runs: - a call to a cluster that accepts the request and answers nothing returns instead of running forever; - a new client, against a cluster whose product check flushes its headers and then stops, returns instead of running forever; - a request through the operator's transport, to a server that accepts the connection and then says nothing, returns instead of running forever; - a body that arrives with its headers and then stops is abandoned; - a call retried on 503 is bounded as a series rather than per attempt; - a body is still readable after the call that fetched it has returned; - the transport carries the handshake bound by value, and the dial bound through the dialer it is built from. Every bound was checked against the mutation that removes it, and all seven turn a test red: the transport wrapper dropped from the constructor, the deadline dropped from `RoundTrip`, the cancel moved off the body there, the deadline dropped from `performRequest`, one call site reverted to `context.Background()`, and the dial and handshake timeouts dropped in turn. Waits are measured from the test goroutine, so an unbounded call fails the test rather than hanging the run until CI kills it. The silent-server test holds its accepted connections and forces a collection while the call is in flight, because an unreachable `net.Conn` is closed by the collector (`net/fd_posix.go` puts a finalizer on `netFD`) and that turns the silent server into one that resets the connection. The full four-patch sequence applies to a pristine v2.8.0 tree in the glob order the build relies on, with no offsets and no fuzz; `gofmt` and `go vet` are clean; and `docker buildx build images/opensearch-operator` builds the image on linux/arm64 with the `go test -race` step green. ### Screenshots Not a UI change. ### Downstream repositories <!-- Cozystack is upstream for repositories that are NOT kept in sync with it automatically, and nothing in CI compares the two sides, so a change here can break them silently. Which change forces what, and which file to touch, is the trigger map in docs/agents/contributing.md under "Downstream Repositories". Walk it before you tick anything. IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM TO FILL IN: 1. Walk the trigger map against the actual diff of this PR, file by file. Do not decide from the PR title or from your memory of what you changed. 2. For every repository you tick, open the follow-up PR there and link it on the same line. A ticked box with no link claims work that does not exist, which is worse than an empty box. Search that repository for an open PR or issue covering it first, and link that instead of filing a duplicate. 3. When the follow-up is out of scope, or needs a decision that is not yours to make, open an issue in that repository instead, link that, and say so in the PR body. Do not push a speculative PR into another repository just to fill a line here. 4. Do not tick "No downstream repository is affected" to make the checklist go away. If you are unsure, leave every box empty and say so in the PR body, so a human decides. --> - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: Walked the trigger map against the diff: one comment block in `packages/system/opensearch-operator/images/opensearch-operator/Dockerfile` and one new file in that image's `patches/`. No package added, renamed or removed, no values key, no schema, no enum, no `hack/` file, no make target, no namespace and no `ApplicationDefinition`. ### Release note <!-- Write a release note: - Explain what has changed internally and for users. - Start with the same `type(scope):` prefix as in the PR title - Follow the guidelines at https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md. --> ```release-note fix(opensearch-operator): time out calls to a cluster's HTTP API, so one unanswered request no longer stops the operator reconciling ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Added timeout handling for OpenSearch cluster connections, including connection, TLS-handshake, response-header, and request deadlines. * Prevented requests from hanging indefinitely when a cluster is unavailable, unresponsive, or stops returning data. * Ensured response bodies remain readable after the initial request completes when applicable. * Bounded the full request lifecycle, including response-body reads and retried calls. * **Tests** * Added coverage for connection, response, retry, and body-read timeout scenarios. * Enabled race detection for relevant package tests. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
ff0b851bbc |
test(vpn): add default render helm unit tests (#4347)
## What this PR does vpn was one of the eight user-facing apps in `packages/apps/` called out in #3631 as shipping with zero helm-unittest coverage. This adds a `tests/` suite pinning the default render and wires the standard `test: helm unittest .` target the sibling charts already use, following the same pattern as #4081, #4074 and #4093. Ran `helm unittest .` locally against the chart, it passes. `make show` needs a live cluster connection through `cozyhr`, which was not available in this environment, so I relied on `helm unittest` for local verification, same as the referenced sibling PRs. ### Screenshots Not applicable, test-only change. ### Downstream repositories Walked the trigger map in docs/agents/contributing.md against the diff: this only adds a test file and a Makefile target inside an existing package, it does not add, rename or remove a package, and it does not touch `values.yaml`, `values.schema.json`, `Chart.yaml` or `README.md`, so none of the listed triggers apply. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note test(vpn): add Helm unit test coverage for the default render ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Tests** - Added automated validation for the VPN deployment’s default configuration, including replica count, container image, service port, and generated resource naming. - **Chores** - Added a convenient command for running the VPN chart’s automated tests. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
4bf95815a8 |
fix(kubernetes): render the aggregation-layer UID header for tenant OIDC (#3539)
## What this PR does Tenant clusters with `spec.oidc.mode: System` or `CustomConfig` authenticate users fine, but a UID does not survive the hop to an aggregated API server. The tenant kube-apiserver publishes `requestheader-uid-headers` into the `extension-apiserver-authentication` ConfigMap only when the flag is set, and the chart never set it. The aggregation layer still sends `X-Remote-Uid`, but the extension server trusts only the header names published in that ConfigMap, so it ignores the one it gets and the call arrives without a UID. The field documentation even told operators to add the flag by hand via `controlPlane.apiServer.extraArgs`. It forwards a UID rather than creating one. ServiceAccount tokens carry one, and for those the aggregation hop starts working. Users authenticated by `spec.oidc.mode: System` still have none, because the generated AuthenticationConfiguration maps `username` and `groups` only; giving them one means mapping a claim, which changes the identity of users who have none today, and is tracked separately. The chart now renders `--requestheader-uid-headers=X-Remote-Uid` on v1.32 and newer, and adds `--feature-gates=RemoteRequestHeaderUID=true` on v1.32, where the gate is still alpha and the apiserver rejects the flag without it. From v1.33 the gate is beta and on by default, so only the header flag is rendered. v1.31 gets nothing: the flag does not exist there, and writing it by hand stops the apiserver from starting. `--emulated-version` in the same passthrough moves that decision, because the gate resolves through the versioned spec at the emulated version. Rendering the gate on every version from v1.32 rather than on v1.32 alone was the alternative, and it would remove the migration step entirely, because an opt-out would then drop two entries instead of one and move the entry count by itself. It loses on what it leaves behind: a redundant gate term on every cluster, which stops the apiserver from starting on the day the gate is retired from the known list. The design also prefers rendering over failing for the same reason the fail branches are narrow, since a cluster stranded on a HelmRelease that no longer converges is a worse outcome than one that never gets the UID fix. The chart reads an operator entry the way the apiserver reads it: header list elements matched whole and untrimmed and folded as `EqualFold` folds them, gate names compared exactly, gate values resolved as `strconv.ParseBool` resolves them, `AllAlpha` and `AllBeta` blankets honoured, an empty value treated as no entry. On v1.32 an entry that says nothing about `RemoteRequestHeaderUID` gets a second one carrying the operator's own terms plus `RemoteRequestHeaderUID=true`. Carrying those terms into the merged copy is what makes it land correctly whether the tenant control plane collapses duplicate flags into the last one or hands both to the apiserver, which merges them in the order it reads them. Tests cover the render matrix over mode, version and operator input: every fail path with its exact message, the ownership and merge paths, duplicate entries in both orders, blanket gates, empty values, and case variants on the header name and on the gate value. The latest-version tenant lane creates its tenant with `spec.oidc.mode: System` and asserts the flag on the rendered KamajiControlPlane, so the tenant apiserver starts with the flag in CI. Nothing asserts the UID arriving at an aggregated apiserver; that needs an extension server inside the tenant and is not in this change. The layer where the count-keyed reset actually bites is the tenant Deployment's arguments, and asserting there needs a live tenant control plane, so it is not in this change either. The rule this follows is that a configuration which renders and boots today keeps rendering. Both flags are new, so an `extraArgs` shape reaching this code was rendering before it existed, and stranding a running tenant on a HelmRelease that no longer converges is a worse outcome than one that never gets the fix. A few shapes do stop rendering, and reaching any of them takes writing `--requestheader-uid-headers` into the passthrough by hand, attempting exactly what the chart now does and getting it wrong; in each the kube-apiserver fails option validation and never starts, so nothing that was working is lost. `docs/oidc-tenant.md` covers these shapes. One known limitation is in there and worth naming here: on v1.33 and newer, adding the documented opt-out is count-neutral in `extraArgs`, and so is removing it again, and on v1.33 and v1.34 adding or removing `--emulated-version=1.31`. The pinned Kamaji build rewrites a running apiserver's arguments only when that count changes and otherwise keeps every flag that has left the list, so either the header flag stays beside a now-disabled gate or the disabled gate stays beside the header flag, and the apiserver refuses to start. Changing the entry count in the same edit is the mitigation, and the fix belongs to the pin rather than to this chart, tracked in #3541. The paths that need no operator edit are clear: turning the feature on for a default tenant moves the count, and a tenant already carrying the opt-out renders byte-identically before and after. Out of scope: `mode: None` still publishes no UID header even though ServiceAccount tokens carry UIDs. That is pre-existing, and widening the condition changes the render for every default tenant. Tracked separately, along with the claim mapping above. Closes #3377 ### Screenshots Not a UI change. ### Downstream repositories - [x] No downstream repository is affected by this change Checked the trigger map against the diff: no package added or removed, no schema field, enum or default changed (description text only), no ApplicationDefinition surface touched, no hack/ layout moved. The managed-apps reference page picks up the new field description through release-time README regeneration. ### Release note ```release-note fix(kubernetes): tenant clusters with `spec.oidc.mode` set to `System` or `CustomConfig` now render `--requestheader-uid-headers=X-Remote-Uid` on the tenant kube-apiserver, plus the `RemoteRequestHeaderUID` feature gate on v1.32, so that an aggregated API server can read the caller's UID out of the request headers. Without that flag the tenant apiserver never publishes `requestheader-uid-headers`, and an aggregated API server ignores the UID header even for callers that have one, ServiceAccount tokens among them. It forwards a UID rather than creating one: users authenticated by `spec.oidc.mode: System` still have none, because the generated AuthenticationConfiguration maps `username` and `groups` only, and that is tracked separately. Adding the flag by hand through `controlPlane.apiServer.extraArgs` is no longer necessary, and an entry of your own is respected: the chart steps aside when yours already carries `X-Remote-Uid` or already enables the gate, merges `RemoteRequestHeaderUID=true` into a copy of a v1.32 `--feature-gates` entry that says nothing about the gate, and renders neither flag when you switch the gate off. The render fails where a `--requestheader-uid-headers` list of your own would stop the kube-apiserver from starting anyway: one that omits `X-Remote-Uid`, one with an empty element, and a list of your own while the gate is off. On v1.33 and newer there is a migration step, and it applies in both directions: the pinned Kamaji build rewrites a running apiserver's arguments only when the number of `extraArgs` entries changes, and otherwise keeps every flag that has left the list. Adding the documented opt-out keeps that number the same, so the header flag already on the Deployment stays beside the now-disabled gate. Removing the opt-out later keeps it the same too, so the disabled gate stays on the Deployment beside the header flag the chart renders again, although your spec no longer shows it. Adding or removing `--emulated-version=1.31` has the same shape both ways on v1.33 and v1.34. The apiserver refuses to start in each case, so change the entry count in the same edit. ``` |
||
|
|
48058dcc74 |
build(opensearch-operator): run the patched tree's tests under -race
The timeout tests drive a listener from one goroutine and inspect what it accepted from another. The plain run passes an unsynchronised access there, so nothing in this repository would catch one. The builder image carries cgo and gcc, and the package images are built natively on x86-64 runners, so the detector is supported where this runs. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
1a2c87635a |
fix(opensearch-operator): bound every call to a cluster's HTTP API
Upstream gives the operator's calls to a cluster no deadline of any kind. The transport it builds carries no dial, handshake or response-header timeout, no http.Client is configured, the calls the client makes on its own behalf pass context.Background(), and the ones that take a context from the caller are handed one with no deadline on it. A cluster that accepts a connection and then says nothing holds such a call for as long as the process lives, and with one worker per controller that single call stops every OpenSearchCluster from being reconciled. The deletion that then waits out its whole termination timeout is the visible end of it. The bound goes on the RoundTripper, the one level every request passes through, the ones opensearch-go builds for itself included: before a client's first request the library runs a product check that carries a context.Background() of its own and decodes the body of the answer, and a client is built per reconcile, so that check runs on every one. The deadline cannot be released when RoundTrip returns, because the body goes back to the caller still open; it rides on the body and is released by Close, the way http.Client applies its own Timeout. That bounds one attempt. opensearch-go retries a request up to three more times on 502, 503 and 504 and the transport's deadline starts over on each, so the calls carry a deadline a second time for the series: through requestContext where the call decodes the answer before it returns, and through performRequest where the still-open body goes back to the caller. The dial and the TLS handshake keep a shorter bound of their own, so an unreachable node fails in a third of the budget with an error that names what went wrong. Each of those bounds has a test that fails when it is removed. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
08bbed3fb5 |
feat(talos-log-collector): add talos-log-collector package (#3260)
## What this PR does Adds `talos-log-collector`, an optional system package: a node-local Vector DaemonSet that receives Talos machine and kernel logs and forwards them to the tenant VictoriaLogs (vlinsert), alongside the container/audit/event logs already collected by monitoring-agents. Talos keeps system and kernel (kmsg) logs inside the OS and does not write them to a host file the Fluent Bit `tail` input could read, so these logs are currently never collected. Vector receives them over the node loopback (Talos pushes to 127.0.0.1:5170). Talos exposes the two streams through independent config paths, and both must point at the loopback socket: `machine.logging.destinations` forwards the JSON-lines service logs, and a `KmsgLogConfig` document forwards the kernel (kmsg) stream (including DRBD kmsg). The node-side setup for both is in the README. Design notes: - The pod stays on the pod network so cluster DNS resolves vlinsert with no extra config. Talos pushes to 127.0.0.1:5170; a `hostPort` bound to `hostIP: 127.0.0.1` publishes that loopback socket into the pod via portmap, keeping the receiver off the host network (no node-wide pod exposure). - The receiver binds inside the pod netns, so a NetworkPolicy is load-bearing: a CiliumNetworkPolicy restricts ingress to the host entity when Cilium is present, otherwise a default-deny-ingress `networking.k8s.io/v1` fallback keeps the socket protected on non-Cilium variants. - Runs non-root, read-only root filesystem, all capabilities dropped. - Optional package (opt-in via `bundles.enabledPackages`): inert until each Talos node is configured to push logs. Node-side setup is in the README. Verified end-to-end on a Talos v1.12 dev cluster: service and kernel logs (including DRBD kmsg) reach VictoriaLogs; pods run non-root with 0 restarts. ### Release note ```release-note feat(talos-log-collector): add talos-log-collector package to collect Talos machine and kernel logs into VictoriaLogs ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an optional node-local Talos log collector (via system bundle packages) that forwards Talos service and kernel logs over loopback to the platform log ingestion endpoint. * Introduced the collector Helm chart (Vector-based) with configurable log level, loopback TCP receive port, digest-pinned image, default resources, and routing target. * Added conditional Cilium network policy support with a safe deny-by-default fallback. * **Security** * Hardened the collector to run non-root with reduced privileges, read-only filesystem, and dropped Linux capabilities (and disabled service account token automount). * **Documentation** * Documented configuration requirements (including kernel log enablement), operational behavior, and teardown steps. * **Build & Release** * Extended the build to include building the collector’s image. * **Tests** * Added Helm rendering/contract tests covering daemonset, configmap, service account, and network policy. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
4fbef512b4 |
fix(opensearch-operator): name the component reconciler a stall sits in (#4333)
## What this PR does The cluster controller runs nine component reconcilers in sequence, and none of them logs unconditionally when it is entered. The TLS one has a line above its work, but it sits on the early return for a cluster with no security section, which `packages/apps/opensearch/templates/opensearch.yaml` never renders, and the rolling restart one opens at `V(1)`, off at the verbosity the operator ships with. So a call that stops returning leaves nothing behind at all: the operator log ends on whatever ran before it, the object stops being reconciled, and from the outside that is indistinguishable from an operator with nothing to do. That is not hypothetical. Two e2e runs on 2026-09-18 ended that way, and the evidence is the operator's own log, read out of the crust-gather host snapshot each failed run uploads, at `snapshots/opensearch/host/namespaces/cozy-opensearch-operator/v1/pod/opensearch-operator-controller-manager-*/operator-controller-manager/current.log`. In run 35343282947 the operator reconciled `OpenSearchCluster` 27 times on a steady cadence, gaps of 10.2s for almost all of them and 26.5s at the widest. Its last line is at 13:51:33.225Z. helm-controller applied the CR revision that moves the cluster onto the chart CA at 13:51:35Z. Nothing was written for the remaining four minutes, the pod stayed Running and Ready with 0 restarts, and the admin certificate the step was waiting on was never re-minted. Run 35333289958, a different branch and a different operator pod, has the same shape: last line 10:56:35.706Z, Apply at 10:56:38Z, widest gap before that 24.2s, then silence. The securityconfig Job had completed successfully in both, and the cluster itself was quiet. #4332 tracks it. A patch on the image now wraps each component so a call still in flight after a minute reports the component's own name and how long it has been running, and keeps reporting on the same interval while it is stuck. A pass through the components takes seconds on a healthy cluster and the controller requeues itself every 30s, so a minute is well past anything normal. Nothing at all is written while a component behaves, which is why this is a watchdog rather than an entry log: logging every entry would put nine lines per cluster into the log on every reconcile. The finalizer path is deliberately left alone, because every `DeleteResources` at this version returns immediately and a watchdog there could only spawn goroutines for calls that cannot block. The e2e lane already collects that log and nothing in it changes here. #4279 put a `podLogs` leg on the operator's own namespace into the opensearch catch, `namespace: cozy-opensearch-operator`, `selector: control-plane=controller-manager`, `tail: 2000`, and this branch sits on top of it. The other `podLogs` leg takes the cluster's own pods in the test namespace, and cozyreport's per-pod walk keeps only pods that are not Running or not Ready, which a stalled operator is neither, so before #4279 these lines reached the job log only through the crust-gather snapshot. This is a diagnostic, not the fix. Where the reconcile stops is still unknown, and the next occurrence should name it. The Dockerfile comment says to drop the patch once the stall itself is understood and fixed. The new code lives in `pkg/reconcilers`, already covered by the image's `go test` step. Five specs hold it: the wrapped result and error pass through untouched, a call that returns before the threshold is never reported, a call that returns before the threshold also takes the watchdog with it, the report names the component and a moving elapsed while the call is in flight, and the report repeats rather than firing once. Six mutations each fail the spec that owns them: dropping `close(done)`, `time.NewTimer(0)`, an empty component name, a frozen elapsed, a fire-once ticker, and a swallowed result. The full four-patch sequence applies to a fresh v2.8.0 tree in the alphabetical order the build relies on, and builds, vets and tests clean there, with five `-race` runs of the new specs. ### Screenshots Not a UI change. ### Downstream repositories Walked the trigger map against the diff: one Dockerfile comment block and one new patch file under `packages/system/opensearch-operator/images/`. No values key, schema, make target or documented workflow changes. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note NONE ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added stall monitoring for OpenSearch component reconciliation. When reconciliation remains in progress beyond the configured threshold, the operator logs the component name and elapsed time at regular intervals. - Monitoring remains silent for promptly completed operations and preserves existing results and errors. - **Documentation** - Clarified reconciliation watchdog behavior and documented why certain resource deletion operations are not monitored. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
95b8549238 |
feat(dashboard): brand the token-proxy login page from the console config (#4255)
## What this PR does Part of #4217. Dashboard branding (title, logo) is only reachable through the authenticated dashboard/kube-api path, so the pre-auth surfaces render unbranded. This PR fixes the first of them: the token-proxy login page — the first screen an unauthenticated user sees on a non-OIDC cluster — hardcoded `<title>Login</title>` and had no branding hookup at all. The branding config lives in the `cozy-dashboard-console-config` ConfigMap, which the SPA reads only through the authenticated kube-api. This mounts that same ConfigMap read-only into the token-proxy container and renders its `titleText`, `logoText` and `logoSvg` on the login page, falling back to the historical unbranded page when the file is absent or unreadable so a cluster without branding is unaffected. The SVG is encoded into an `<img>` data URI rather than inlined, which keeps any embedded script inert. The OIDC branch is untouched: its login is served by Keycloak, already branded through the realm login theme. The unauthorized/access-denied screens rendered by the SPA itself (for a logged-in user without kube-api RBAC) are handled in a separate PR. Verified with `go test`, `go vet`, `gofmt`, and `helm unittest` (branding mount asserted present in the non-OIDC branch and absent in the OIDC branch). ### Downstream repositories The diff touches only `packages/system/dashboard` (the in-repo token-proxy image and its chart). No app is added, renamed or removed, no `values.schema.json` / enum / default changes, no CRD or namespace changes, and the `config.json` branding keys are unchanged. Nothing in the trigger map applies. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note feat(dashboard): the token-proxy login page now renders the deployment's branding (title and logo from `branding.titleText` / `branding.logoText` / `branding.logoSvg`) instead of the hardcoded default, so unauthenticated users on non-OIDC clusters see a branded sign-in screen. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added deployment branding to the login page, including a custom title, logo, and heading. * Branding is automatically loaded from the dashboard configuration when available. * Login pages continue to display a default unbranded experience when branding configuration is unavailable or invalid. * **Tests** * Added coverage for branded and unbranded login-page rendering. * Added validation for branding volume mounts across authentication modes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
12e64a99cd |
fix(rabbitmq): right-size the default resources preset to s1.nano (#3936)
## What this PR does
Raises the RabbitMQ default resource preset from `t1.nano` (128Mi
memory) to `s1.nano` (250m CPU / 512Mi memory).
A default-sized RabbitMQ 4.2 broker on Cozystack 1.6.1 was OOMKilled
with exit 137, reached 10 restarts, and never became Ready. A canary on
a larger preset reached 3/3 Ready with zero restarts; its working set
peaked at 125.8Mi. Against the 128Mi `t1.nano` ceiling that is 98% of
the limit in steady state, and cgroup accounting (working set plus page
cache) crosses it under load. The canary and its storage were removed
after validation.
### Sizing
`s1.nano` keeps the CPU of `t1.nano` (250m) and quadruples memory to
512Mi — a 4x margin over the observed 125.8Mi peak.
An earlier revision of this PR proposed `u1.nano` (1Gi). Review
correctly objected that 1Gi reserves 8x the observed working set with no
measured peak to justify it, and asked to either report that peak or
right-size the default. The peak is reported above, and the default is
right-sized to the smallest preset that clears the ceiling with a real
margin rather than to the first one that is comfortably large.
### Upgrade impact
cozystack-api applies OpenAPI schema defaults on the read path and
stores what the client sent: `Get` runs `applySpecDefaults` on its way
out (`pkg/registry/apps/application/rest.go:1232`) and the write puts
`app.Spec` into `HelmRelease.spec.values` (`rest.go:1652`). That splits
existing brokers in two.
**Brokers with no stored `resourcesPreset`** pick up the new chart
default on the next Helm reconcile. For them this upgrade is:
- a rolling restart of every replica, and
- a rise in guaranteed memory reservation from 128Mi to 512Mi per
replica, because the sanitize helper sets memory request equal to limit
— roughly 1.1Gi extra reserved per 3-replica instance.
**Brokers whose values went through a read-modify-write keep `t1.nano`
and are not resized.** `Update` opens with `r.Get` (`rest.go:489`), so a
GET-then-PUT — `kubectl edit` is the unambiguous case, and any client
that reads before writing behaves the same — freezes the schema default
of the moment into the stored values, and a stored value beats the chart
default. Those brokers are not restarted, not resized, and keep hitting
the ceiling this PR raises.
Nothing downstream can separate a deliberate `t1.nano` from one a round
trip baked in, so this is documented rather than migrated. After the
upgrade the affected brokers are the ones that still report the old
preset:
```
kubectl get rabbitmq -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,PRESET:.spec.resourcesPreset
```
The same command before the upgrade tells you nothing: the cozyrd
default is still `t1.nano` then, so the read path fills it in for both
groups. Afterwards, anything reading `t1.nano` carries a stored value;
setting `resourcesPreset: s1.nano` explicitly on the ones that should
move is the fix.
PVCs are untouched, so there is no data loss, but node headroom should
be checked before upgrading.
Also worth knowing when reading the parameter table: the preset does not
only apply to a fully omitted `resources`.
`cozy-lib.resources.defaultingSanitize` merges it key by key, so a
broker pinned to `resources: {cpu: 700m}` keeps its CPU and moves from
128Mi to 512Mi with everyone else. The field descriptions now say so,
and a test pins it.
The values, generated schema, ApplicationDefinition, typed API, and
README all carry the same default. Tests cover the new default, an
explicit legacy override, explicit resources, and a partially set
`resources` map inheriting the preset for what it leaves out.
Validation:
- `helm template` with the chart's `cozy-lib` dependency renders `limits
250m / 512Mi` and `requests 0.025 / 536870912` for the default; an
explicit `t1.nano` renders `128Mi / 134217728`; explicit `700m / 2Gi`
renders `0.07 / 2147483648`. The unit test asserts exactly these values.
- Provider counterpart: cozystack/terraform-provider-cozystack#34.
### Screenshots
Not a UI change.
### Downstream repositories
The default is restated by the Terraform provider, so the linked
provider PR updates it and documents the state-plan implication. No
other trigger-map entry matches this diff.
- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [x]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
https://github.com/cozystack/terraform-provider-cozystack/pull/34
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
### Release note
```release-note
fix(rabbitmq): raise the default resource preset from t1.nano to s1.nano so default-sized brokers are no longer OOMKilled. Brokers with no stored `resourcesPreset` are restarted on upgrade and their guaranteed memory reservation grows from 128Mi to 512Mi per replica; check node headroom before upgrading. Brokers whose values already pin `resourcesPreset`, including ones where a read-modify-write through the API froze the old default into them, keep their old size: list them after the upgrade with `kubectl get rabbitmq -A -o custom-columns=NAME:.metadata.name,PRESET:.spec.resourcesPreset` and set `resourcesPreset: s1.nano` explicitly on the ones that should move. The preset also fills any resource a partially set `resources` leaves out, so a broker pinned to cpu alone moves too. A tenant that opted into `resourceQuotas` and sits near its memory ceiling has the extra reservation rejected by quota admission, which shows up as a StatefulSet that never scales rather than as pods left Pending.
```
|
||
|
|
05b121809c |
feat(dashboard): brand the token-proxy login page from the console config
The dashboard branding (title, logo) lives in the cozy-dashboard-console-config ConfigMap, which the SPA reaches only through the authenticated kube-api. The token-proxy login page — the first screen an unauthenticated user sees on a non-OIDC cluster — had no access to it and rendered the hardcoded default title and heading, defeating branding for customer-facing deployments. Mount that ConfigMap read-only into the token-proxy container and render its titleText, logoText and logoSvg on the login page, falling back to the historical unbranded page when the file is absent or unreadable so a cluster without branding is unaffected. logoSvg is already base64 in the config — the SPA's Logo.tsx consumes it that way — so it is served verbatim through an <img> data URI, never re-encoded, which keeps any embedded script inert. The OIDC branch is untouched: its login is served by Keycloak, already branded through the realm theme. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
7d07bbe3ce |
[kafka] Migrate from ZooKeeper to KRaft mode (#2352)
## Summary - Replace ZooKeeper-based Kafka deployments with KRaft (Kafka Raft) metadata mode - Existing ZooKeeper instances migrate automatically and in place via a Helm `pre-install,pre-upgrade` hook Job that drives Strimzi's ZK-to-KRaft migration state machine - New instances deploy directly in KRaft mode with broker and controller `KafkaNodePool`s - The node pools are owned by the migration hook, not by regular chart manifests (see "Why the pools are hook-managed") - Kafka version is user-configurable (v3.9 default, v3.8 available) - Strimzi operator is unchanged in this PR (stays at 0.45.1-rc1); 0.45.x is the last line that supports ZooKeeper→KRaft migration - ZooKeeper removed from the dashboard UI (ApplicationDefinition) ## Migration strategy Both fresh installs and ZooKeeper→KRaft migrations are driven by a single Helm hook Job (`templates/migration-hook.yaml`, `pre-install,pre-upgrade`) that owns the `KafkaNodePool`s. On every install and upgrade the Job: 1. Ensures both node pools exist (idempotent): a broker pool named exactly `kafka` for a migrating cluster — so Strimzi adopts the existing `<cluster>-kafka-N` brokers and their PVCs in place — or `b-<hash>` for a fresh cluster, plus a controller pool `c-<hash>`. The controller pool pins `strimzi.io/next-node-ids` above the broker range so the broker pool keeps node IDs `[0..N-1]` and Strimzi cannot hand the existing broker IDs to the fresh controllers. The shared `kafka` name is claimed atomically (via `kubectl create`) so only one migration per namespace can win it. 2. Inspects the Kafka CR: no CR → fresh install (pools ensured; the main apply then creates the KRaft Kafka CR, nothing to migrate); already `strimzi.io/kraft=enabled` or `status.kafkaMetadataState: KRaft` → done; any other read error → fail closed. Otherwise (a ZooKeeper cluster) it migrates. 3. Annotates `strimzi.io/node-pools=enabled` then `strimzi.io/kraft=migration`, polls `status.kafkaMetadataState` through `ZooKeeper → KRaftMigration → KRaftDualWriting → KRaftPostMigration → PreKRaft → KRaft`, and only once a safe state is reached flips `strimzi.io/kraft=enabled`. If it times out in an intermediate state it aborts without finalising, so Helm fails the upgrade and the next reconcile retries from where it left off. Data is migrated in place: brokers keep their node IDs and reuse their existing PVCs, nothing is copied. Migration is one-way — Strimzi has no supported KRaft→ZooKeeper rollback once complete, so lowering the `version` field or rolling the app back to a pre-KRaft chart is unsupported (take a backup first). ### Why the pools are hook-managed The hook must create the pools and walk the migration state machine *before* the main release apply. A resource a hook creates cannot also be a regular chart manifest across the ZK→KRaft boundary: Helm's main apply finds the pool live in the cluster but absent from the previous (ZooKeeper) release revision and aborts with `original object KafkaNodePool ... not found`, leaving the release `failed` even though the cluster migrated fine (reproduced under server-side apply, the helm-controller default). Managing the pools only from the hook avoids that, so `templates/kafkanodepools.yaml` and the deployed-version gate ConfigMap (`templates/version.yaml`) are removed and the pools are applied idempotently by the Job on every install and upgrade. ## Bug fixes included - ZooKeeper `metricsConfig` referenced wrong ConfigMap key (`kafka-metrics-config.yml` instead of `zookeeper-metrics-config.yml`) - ZooKeeper `storageClass` used `kafka.storageClass` instead of `zookeeper.storageClass` - WorkloadMonitor used `.Values.replicas` instead of `.Values.kafka.replicas` - `validateNameLength` now caps a Kafka app name at the KRaft controller pod-hostname budget (`<release>-c-<hash>-<id>` must fit 63 chars), instead of admitting names that later fail the chart render forever ## What's next A follow-up PR will upgrade Strimzi to 0.51.0 and Kafka to 4.2 once all instances are confirmed migrated to KRaft. That PR is already prepared on branch `KRaft-upgrade-strimzi-051`. ## Test plan - [x] `helm template` renders correctly for new KRaft instances (no ZooKeeper refs) - [x] `helm template` with custom values (1 replica, external, topics, v3.8) - [x] Version validation rejects unsupported versions via JSON schema - [x] helm-unittest: 86/86 across 9 suites - [x] `hack/kafka-migration-hook.bats`: 12/12 — the real hook script executed against a stub kubectl (resolution branches, fail-closed reads, atomic collision guard, finalize guard) - [x] Operator template renders with the unchanged Strimzi 0.45.1-rc1 - [x] Live migration on a disposable cluster under server-side apply: fresh install → healthy KRaft, release `deployed`; in-place migration of a 20000-message ZooKeeper cluster → release `deployed`, broker PVCs adopted in place (same UIDs), and the KRaft data byte-for-byte identical to the pre-migration ZooKeeper data (matching sha256 over every `partition:offset:key:value`) ### Release note ```release-note [kafka] Enable KRaft metadata mode with automatic, in-place ZooKeeper-to-KRaft migration. Kafka version is now configurable (v3.9, v3.8). The Strimzi operator is unchanged (0.45.1-rc1). ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added chart-level Kafka version selector (default v3.9) and KRaft-aware node pool rendering with controller storage sizing. * **Updates** * Default Kafka appVersion updated to 3.9.0; operator and image tag defaults aligned to 0.45.0. * Templates now omit ZooKeeper resources and metrics when KRaft is in use. * **Tests** * New Helm unit tests for Kafka CR, node pools, metrics, topics, dashboard mappings, and workload monitors. * **Documentation** * Values, schema, and README updated to document new version and controller storage options. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
ad4ea5c937 |
feat(keycloak): publish only the named realms on the login host (#4363)
## What this PR does `ingress.adminHost` moves the admin console and Administration REST API onto their own hostname and narrows the public login route to `/realms`, `/resources` and `/.well-known`. That `/realms` prefix publishes every realm the deployment has, `master` among them, so the super-admin login page and token endpoint stay reachable from the internet on the login hostname — a brute-force target the admin split was meant to remove. The Keycloak reverse proxy guide marks `/realms/master/` as internal-only. The login route now enumerates the realms that are meant to be public: the platform realm (`authentication.oidc.realmName`, default `cozy`), which every component's issuer URL points at and which is therefore always published, plus whatever a deployment adds in the new `ingress.exposedRealms`. Each realm is published as an exact path plus a prefix carrying the trailing slash. ingress-nginx renders `pathType: Prefix` as a plain nginx prefix location rather than the element-wise match the Ingress API specifies, so a lone `/realms/cozy` would also serve `/realms/cozy-admin` there; the pair bounds the match on the segment and is equivalent to element-wise matching on a conformant controller and on Gateway API. `master` is refused outright — both in `ingress.exposedRealms` and as `authentication.oidc.realmName`, where the guard sits next to the existing realm-name checks so it applies whether or not a separate admin hostname is configured. Naming that realm puts the platform clients, the cluster-admin roles and every end user inside the realm that holds the Keycloak super-admin. This is a path boundary, not a network one. `KC_HOSTNAME_ADMIN` never was a security boundary, `expose-ingress-admin` defaults to the public Gateway/ingressClass, and with `adminHost` unset the login route still serves `/`. Which hostnames the edge accepts and where the admin route attaches decide the rest. **Behavior change**, confined to deployments that already set `ingress.adminHost`: a realm published on the login hostname beyond the platform one now answers 404 until it is listed in `ingress.exposedRealms`. The `master` guard on `authentication.oidc.realmName` breaks nobody, since that parameter has not shipped in a release yet. ### Screenshots Not applicable — no UI change. ### Downstream repositories - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: Walked the trigger map against the diff: it touches `packages/system/keycloak` and the realm-name guard in `packages/core/platform/templates/apps.yaml`. No app package, no `packages/core/platform/values.yaml` key the Ansible role sets, no platform variant or component, no `ApplicationDefinition`, no `hack/` layout, no telemetry metric, no vendored chart contract. ### Release note ```release-note feat(keycloak): with `ingress.adminHost` set, the login hostname publishes only the platform realm and any realm listed in the new `ingress.exposedRealms`, keeping the `master` realm login page and token endpoint off the public route. A deployment that serves additional realms on that hostname must list them. `authentication.oidc.realmName` no longer accepts `master`. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added `ingress.exposedRealms` to control which additional identity realms are reachable through the login hostname. - Login routes now target the platform realm and configured exposed realms with precise realm-specific paths. - Duplicate realm names are handled automatically. - **Bug Fixes** - Prevented publishing the privileged `master` realm. - Added validation for invalid realm names and configurations. - Enforced the gateway route limit, supporting up to 31 published realms. - **Documentation** - Updated configuration guidance for realm exposure and security restrictions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
176dd78c1a |
feat(dashboard): serve console branding without kube-api access (#4256)
## What this PR does Part of #4217. The SPA fetched its branding (title, logo) only by reading the `cozy-dashboard-console-config` ConfigMap through the kube-api. That read already works for any signed-in user — `console-rbac.yaml` binds `get` on it to `system:authenticated` — but it still depends on the kube-api being reachable and on the apiserver authenticating the session's token. A dashboard login whose token the apiserver rejects (an OIDC session the kube-api does not accept) gets no branding at all. (The unauthenticated login page is a separate surface, branded in #4255.) This mounts that ConfigMap read-only under the nginx web root and serves it at `/branding/config.json`, and reads that static asset first in `loadConfig`, falling back to the kube-api ConfigMap for charts that predate the mount (their static path serves the SPA `index.html`, whose non-JSON body trips the fallback). Branding then resolves with no kube-api call at all, so it renders regardless of whether the apiserver authenticates the session and without the `console-config-reader` RBAC. Because `config.json` is now a static asset reachable by anyone past the proxy, it is restricted to the documented white-labeling keys: the branding map also carries Keycloak-only keys, including `loginThemeImage`, which can point at a private registry. Keycloak reads those from `_cluster.branding` directly. The mount uses an `items` selector so only `config.json` is exposed under the web root. Verified with `pnpm typecheck`, `pnpm test` (7 `loadConfig` cases), `eslint`, and `helm unittest` (mount + `items`, the `config.json` key the mount serves, white-label keys kept incl. `tenantText`, Keycloak-only keys absent). Also validated live on a dev cluster: `console-rbac.yaml` binds the ConfigMap read to `system:authenticated` (`kubectl auth can-i get` this CM as an unbound user → yes; another CM → no), and a throwaway pod on the real console image serving the mounted ConfigMap returns `200 application/json` at `/branding/config.json` while an `items`-excluded key falls through to `index.html`. ### Screenshots No visual regression to show in a static render: this changes where the branding config is fetched from, not how it is displayed. ### Downstream repositories The diff touches only `packages/system/dashboard` (the in-repo console image and its chart). No app is added, renamed or removed, no `values.schema.json` / enum / default changes, no CRD or namespace changes, and the branding keys the SPA reads are unchanged — only the transport (static asset vs kube-api) and a narrowing of which keys are served. Nothing in the trigger map applies. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note feat(dashboard): the console serves its branding as a static asset (`/branding/config.json`) read with no kube-api call, so branding renders even when the apiserver does not authenticate the session and without the console-config-reader RBAC; the served file is narrowed to the documented white-labeling keys. ``` |
||
|
|
187e4ce12b |
test(e2e): assert the tenant OIDC UID header on the control plane
The render matrix proves the chart emits --requestheader-uid-headers, but nothing checked that the flag survives the path a real tenant takes: aggregated API admission of spec.oidc, the HelmRelease, and the rendered KamajiControlPlane. Assert it in both OIDC helpers, next to the existing --authentication-config check on the same resource. Read the arg list once into a variable and grep the capture twice. The existing check consumed a pipeline, which is single-use, and a second kubectl call would double the management-cluster reads these helpers already make. Only the header flag is asserted. The chart also renders --feature-gates=RemoteRequestHeaderUID=true, but only on v1.32 where the gate is still alpha, while this lane takes the highest version in the map, which is above that. The header flag is the part that holds wherever the flag exists at all. The bats case for the missing flag runs the helper as a standalone subshell under set -e rather than as an if condition. Shells suppress errexit for a command whose status is being tested, and that reaches inside the subshell, so the if spelling reports success and pins nothing. The binding query next to these assertions piped kubectl straight into sort and so returned sort's status. Its callers capture it in a command substitution, where errexit does not reach, so a failed query read as an empty binding list. It now returns kubectl's failure itself. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
97832f3a40 |
docs(kubernetes): explain the aggregation-layer UID hop for tenant OIDC
A tenant cluster with OIDC on authenticates users fine while every call through an aggregated API server reaches it without the caller's UID, and nothing in the operator guide connected the two. Describe the hop, what the chart now renders per Kubernetes version to make it work, and how a hand-written flag in the extraArgs passthrough interacts with it. The migration step is described in both directions. The pinned tenant control plane rewrites a running apiserver's arguments only when their count changes and otherwise keeps every flag that has left extraArgs, so removing an opt-out or an --emulated-version entry strands a flag exactly as adding one does, and the operator can no longer see it in their own spec. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
f4e923aa82 |
docs(kubernetes): restate the apiServer extraArgs contract for OIDC
The extraArgs field documentation told operators to add --requestheader-uid-headers themselves, which the chart now renders whenever OIDC is on. Describe what it renders per version, when it defers to an operator entry, when it merges into one, and that it still refuses to render shapes the apiserver would not start on. The version field says it too: choosing v1.32 is what makes the chart render the feature gate next to extraArgs, and a reader of that field alone would otherwise not learn that the two interact. The package's hack/update-versions.sh writes that description itself on every make update, so it carries the same text, or the next version bump would drop the sentence again. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
d870b392fa |
fix(kubernetes): render the aggregation-layer UID header for tenant OIDC
A tenant kube-apiserver publishes requestheader-uid-headers into the extension-apiserver-authentication ConfigMap only when the flag is set, and the chart never set it. Without that key the request-header authenticator on an aggregated apiserver does not know which header carries the UID and ignores it, so a caller who has a UID is served without one. Operators had to add the flag by hand through the controlPlane.apiServer.extraArgs escape hatch. This forwards a UID the caller already has and does not create one. ServiceAccount tokens carry a UID and gain from it. OIDC users authenticated through spec.oidc.mode: System do not: the AuthenticationConfiguration the chart generates maps username and groups only, and with no claimMappings.uid upstream returns an empty UID (plugin/pkg/authenticator/token/oidc/oidc.go, getUID, read at v0.35.0), so there is nothing to carry. Mapping a claim would give those users a UID and change the identity of users who have none today, which is a separate decision and is tracked separately. The half read rather than run: that an existing UID is lost without the published key comes from the controller writing the ConfigMap and the request-header authenticator consuming it, not from a live cluster. That OIDC users have no UID is unambiguous in the source named above. Render --requestheader-uid-headers=X-Remote-Uid whenever OIDC is on, plus --feature-gates=RemoteRequestHeaderUID=true on v1.32, where the gate is still alpha and the apiserver rejects the flag without it. v1.31 gets nothing: the flag does not exist there and an unknown flag is fatal to the apiserver. The deciding version is the effective one, not the image tag. --emulated-version in the same passthrough moves it down, and the gate resolves at the emulated version through featureSpecAtEmulationVersion (featureSpecAtEmulationAndMinCompatVersion by v1.35), so an emulated 1.32 needs the gate line even on a v1.35 cluster, and below 1.32 the gate is PreAlpha and cannot be enabled at all, which leaves nothing safe to render. Both flags are new, so every extraArgs shape reaching this code was rendering before it existed, and turning one of those into a render error would strand a running tenant on a HelmRelease that stops converging while its control plane carries on. The chart therefore yields to an operator entry that already satisfies the requirement, renders nothing at all when the operator turned RemoteRequestHeaderUID off, and otherwise merges its gate term into a copy of the operator's --feature-gates value. Carrying that value into the merged entry decides the gate whether the tenant control plane collapses duplicate flags into the last one or hands both to the apiserver, which merges them in the order it reads them. Operator entries are read the way the apiserver reads them: header list elements matched whole and untrimmed, folded as EqualFold folds them, and refused when one is empty after trimming, as checkForWhiteSpaceOnly refuses it; gate names compared exactly; gate values resolved as strconv.ParseBool resolves them; blanket AllAlpha and AllBeta honoured; an empty value treated as no entry at all. Each flag is judged by its last entry carrying a value. The apiserver's CSV reader strips quotes the chart does not parse, so quotes on that entry make the chart render nothing, while quotes on an earlier entry the collapse drops do not. The render fails only where a uid-headers list of the operator's describes a control plane that already refuses to start: one that omits X-Remote-Uid, one with an element that is empty after trimming, and any such list while the gate is off. Withholding the chart's own flag rescues none of them, because the flag the apiserver rejects is the operator's. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
9efe78ce44 |
test(e2e): hold the tenant-cluster install wait to an ordering rule (#3802)
A test that creates a tenant `Kubernetes` cluster must not end while that release is still installing. The kind drops the install wait on its main resources, but the chart's blocking post-install hook still holds the install open, so a test that asserts and ends hands the rest of it to teardown. Teardown then reds on a step that tests nothing. Reading the cluster mid-install is fine. Ending mid-install is not. `hack/e2e-chainsaw/_lib/run-kubernetes.sh` already waits for the parent release before its tenant-cluster assertions, and that wait is older than this change. Nothing held it there. This adds the guard that does. The guard reads the script, not Chainsaw fixtures: the cluster comes from a heredoc, so a guard walking `apply.file` would find nothing to check and pass. It requires the CR, then a readiness wait on the parent release, then the assertions the test ends on. The wait has to block, and it has to be unconditional in the body that holds it. One more case holds this script to being the only creator of that kind, since a second one would be governed by no rule here. The ordering is read within one function body, so splitting `run_kubernetes_test` reds the unit lane until the rule is pointed at the new shape. Timeout length stays with review. The floor depends on where the wait sits, and the script text does not carry that. Which spellings each matcher takes, which rewrites it refuses instead of judging, and the blind spots it names are written in the guard's header next to the code, each with its mechanism and whether it fails loudly or quietly. Fixtures mark their own lines, so a probe resolves by name instead of by a line number that drifts. No runtime code here: the guard, its fixtures, and the conventions that state the rule. ```release-note NONE ``` |
||
|
|
422b6f65ea |
test(vpn): add default render helm unit tests
vpn was one of the remaining user-facing apps with no helm-unittest coverage. Signed-off-by: Akanksha Trehun <akankshatrehun@gmail.com> |
||
|
|
41ef4a6437 |
fix(ci): teach the overlay to repair an unstamped images/*.tag ref (#4362)
## What `hack/overlay-main-images.sh` walks `images/*.tag`, but every branch of its line classifier needs either a YAML key or an `@sha256:`. A `.tag` file holds one bare reference and nothing else, so until a build stamps it the line has neither, the drift guard reads it as a non-ref change, and the committed reference survives. For a package onboarded with the `:v0.0.0` placeholder this repo already uses for a new image, that surviving reference is one no registry serves. The overlay is the only thing that would replace it for a PR that does not rebuild the package, so once such a package is on main every full-suite run that skips its build keeps the placeholder. The same placeholder inside a `values.yaml` repairs itself, because there it sits under an `image:` key the classifier recognises. The difference is the file shape, not the placeholder. ## Reproduction A fixture holding a committed bare tag against an artifact copy carrying a stamped one: ``` $ cd fixture/fakerepo && sh ../../hack/overlay-main-images.sh ../mainpkgs '[]' '' overlay: packages/system/x/values.yaml -> current-main drift (non-ref change) in packages/apps/x/images/x.tag -> keeping committed ref ``` Both files differ only in their reference. The `values.yaml` one is repaired; the `.tag` one is not. Adding any digest to the same placeholder makes it pass, which is what narrows this to the classifier rather than the file. ## Why it has not bitten yet 17 of the 19 `.tag` files in the tree carry `@sha256:` and pass on that branch alone. The only other digest-less one is `packages/extra/seaweedfs/images/seaweedfs-cosi-driver.tag`, and it survives only because it is third-party: `build-main` never rebuilds it, the artifact copy is byte-identical, and the classifier is never reached. The first digest-less first-party `.tag` is what turns the gap live, and one is coming in #3426. ## The fix `hack/promote-rewrite-tags.sh` already carries a second match branch for exactly this shape, with a comment saying why ("the second branch is what catches a bare `.tag` file, whose content has no YAML key at all"). This gives the overlay the same one, scoped to `*.tag` so an unkeyed line in a `values.yaml` still counts as drift, because only in a `.tag` file is a whole line known to be a reference and nothing else. Two tests in `hack/overlay-main-images_test.bats`: the unstamped placeholder is overlaid, and a `.tag` whose non-reference line differs still counts as drift, so the rule cannot quietly widen into "every line in a `.tag` file is a reference". Removing the fix fails the first one. ```release-note NONE ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Fixed image overlay handling for `.tag` files containing bare image references, allowing current references to be applied correctly. - Preserved drift detection when `.tag` files also contain unrelated, non-reference changes. - **Documentation** - Clarified how image-reference tooling handles bare references and package-tree processing. - **Tests** - Added coverage for bare-reference overlays and mixed reference/comment changes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
78f01d5542 |
docs(rabbitmq): point the preset section at the shared size matrix
The preset table in this README listed the seven legacy aliases only, and its CPU column disagreed with cozy-lib on every row, so a reader looking up what the new s1.nano default buys them found neither the name nor a correct number. Nine app READMEs already replaced that table with the two paragraphs that describe the <series>.<size> convention and link the full matrix in docs/operations/resource-presets.md; this README now carries the same text. Assisted-by: LLM Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com> |
||
|
|
6d5666f7e6 |
fix(rabbitmq): describe the preset as filling what resources leaves unset
cozy-lib merges the preset under resources key by key, so a partially set resources map still takes the preset for everything it omits: with resources.cpu=700m alone the broker renders 700m of CPU against the preset's 512Mi of memory, and this upgrade moves that broker from 128Mi to 512Mi as well. The description said the preset applies when resources is omitted, and cozyvalues-gen copies it verbatim into values.schema.json, the README parameter table and the cozyrd openAPISchema the dashboard renders, so one sentence made the claim on three surfaces. The suite covered a fully omitted resources map and one that sets both cpu and memory, which is the one shape that does not exercise the merge. The new case pins it: reverting defaultingSanitize to replace the preset instead of merging over it turns that case red and leaves the other three green. Assisted-by: LLM Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com> |
||
|
|
03394bd5fe |
chore(rabbitmq): drop the test target the rebase duplicated
main already carries a test target at the bottom of the Makefile; the rebase kept the earlier copy at the top and GNU make warns about the override. The Makefile matches main again. Assisted-by: LLM Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com> |
||
|
|
cf749988a4 |
fix(rabbitmq): right-size the default resources preset to s1.nano
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com> Assisted-by: LLM |
||
|
|
c0ff783026 |
fix(hack): make bats unit tests pass on a BSD/macOS checkout (#4284)
## What this PR does Refs #4272 (Defects 1 and 2). On a clean checkout of `main`, `make unit-tests` is red out of the box on macOS for reasons that have nothing to do with the change under test: the BSD userland shipped with macOS differs from the GNU tools on the Linux CI runners, and CI never sees any of it. This PR closes the two worst of those, both scoped to `hack/`. **Defect 1 — the runner cannot parse a non-ASCII `@test` title.** `hack/cozytest.sh` rebuilds each test's function name by walking its title one character at a time with awk `substr()`. BSD awk aborts that walk on the first multibyte byte (`towc: multibyte conversion failure`) rather than returning it, so a file carrying a non-ASCII `@test` title never parses and not one of its tests runs — silently, because the suite only reports that the runner errored. Four `hack/*.bats` files carry such a title today. The fix pins `LC_ALL=C` on all three awk passes (title parse, run selection, and the exit-code scan of the captured log) so each reads the input byte-wise; the parse and selection passes must share the ctype or they would derive mismatched function names for one title. A new regression test, `hack/cozytest-nonascii-title.bats`, builds a multibyte-titled fixture at runtime and asserts the runner actually runs it; it is red against the pre-fix runner (mutation-verified) and green after. **Defect 2 — GNU-only `sed -i`.** `hack/nightly-mirror.sh`, `hack/promote-rewrite-tags.sh`, and the fixture builder in `hack/promote-rewrite-tags_test.bats` edit files in place with `sed -i`. BSD sed reads the argument after `-i` as a mandatory backup suffix, so `sed -i -e` and `sed -i -E` swallow the following flag as that suffix and the edit either errors or silently no-ops. Each is rerouted through a temp file, portable to both seds. The `nightly-mirror.sh` rewrite additionally relied on the GNU `\?` optional quantifier, which BSD BRE reads as a literal `?` so a bare host was never matched; it is replaced with the POSIX `\{0,1\}`, equivalent on both. All three suites are green on macOS after the change, and behaviour on GNU sed is unchanged. ### Scope This PR is deliberately scoped to Defects 1 and 2. Defect 3 from the issue — a GNU-only `date -d` in `hack/seaweedfs-naming-audit.sh` — is tracked separately as #4273 and is left untouched here. For transparency: with Defect 1 fixed, several `hack/*.bats` suites that previously never ran now run, and a handful still fail on macOS because they depend on GNU coreutils `timeout`, which base macOS lacks (`capture-dataplane`, `cozytest-capture-gate`, `run-kubernetes-join-timing_test`, `run-kubernetes-runner-cpu_test`, `runner-identity`). Those failures are pre-existing — identical before and after this change when the runner is swapped in place — and are a distinct defect from the two fixed here, so they are out of this PR's scope. A fully green `make unit-tests` on macOS additionally depends on #4273 and on that separate `timeout` gap. ### Verification All fixes were verified on a genuine macOS host (Darwin, BSD awk 20200816 / BSD sed). `hack/cozytest-nonascii-title.bats`, `hack/nightly-mirror_test.bats` (5 tests), and `hack/promote-rewrite-tags_test.bats` (17 tests) are green; the two files the issue reports as running zero tests (`overlay-main-images_test.bats`, `promote-gate-contract.bats`) now yield 13 and 15 `Test OK`, matching the issue's numbers. Each fix is mutation-proven: reverting it turns the corresponding suite red again. ### Downstream repositories The diff touches only internal `hack/` tooling — the bats test runner and two release scripts. No downstream repository vendors or restates these: `cozystack/ccp` and `cozystack/external-apps-example` gate on `hack/package.mk`, `hack/common-envs.mk` and `hack/update-crd.sh`, none of which this PR changes, and nothing renames or moves a file or alters what a make target does. - [x] No downstream repository is affected by this change ### Release note ```release-note fix(hack): make the bats unit-test runner parse non-ASCII `@test` titles under BSD awk, and route the in-place edits in `nightly-mirror.sh` and `promote-rewrite-tags.sh` through a temp file so they work under BSD sed — closing two of the host-tool gaps that make `make unit-tests` red on a clean macOS checkout ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved test runner compatibility with non-ASCII test titles and output. * Improved text-processing and file-rewrite behavior across BSD and GNU environments. * Preserved file permissions and metadata during automated rewrites. * Improved handling of host values and release tags across supported platforms. * **Tests** * Added coverage for non-ASCII test titles. * Updated rewrite tests for portable BSD and GNU tooling. * **Documentation** * Clarified supported BSD and GNU command-line tool compatibility. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
6a34c9f037 |
fix(opensearch-operator): name the component reconciler a stall sits in
The cluster controller runs nine component reconcilers in sequence and none of them logs unconditionally when it is entered: the TLS one's only line above its work sits on the no-security early return, which the chart in this repository never renders, and the rolling restart one opens at V(1), off at the verbosity the operator ships with. So a call that stops returning leaves nothing behind at all. The operator log ends on whatever ran before it, the object is silently no longer reconciled, and from the outside that is indistinguishable from an operator with nothing to do. Two e2e runs ended exactly that way, the operator going quiet at the moment the chart rewrote the CR and never coming back: https://github.com/cozystack/cozystack/issues/4332. Each component is now wrapped so a call still in flight after a minute reports its own name and how long it has been running, and keeps reporting on that interval while it is stuck. A single line dated at the start of a stall does not answer the question a reader has five minutes in, which is whether the call is still there. Nothing is written while a component behaves, so a healthy cluster costs no extra lines. The finalizer path is left alone: every DeleteResources at this version returns immediately, so a watchdog there could only spawn goroutines for calls that cannot block. A diagnostic rather than a fix: drop it once the stall itself is understood. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
bad152443e |
fix(kafka): correct the stale co-namespaced migration render error
The brokerPoolName render guard still told operators to "migrate one at a time or use separate namespaces" when a sibling cluster owned the shared "kafka" pool. That predates automatic name freeing: the migration hook now drains the first cluster off the shared pool with a Cruise Control rebalance and releases the name, and the waiting cluster's HelmRelease retries into it — every co-namespaced Kafka reaches KRaft, no manual action required. Reword the fail message and the helper comment to describe the transient wait-and-retry, and update the unit test's error pattern to match. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
0b9332877e |
test(kafka): drop the false-green ZK->KRaft migration chainsaw suite
The kafka-zk-to-kraft-migration suite did not exercise the pre-upgrade hook, it reimplemented a simplified flow beside it: a controller pool named "controller" instead of the hashed c-<hash>, no strimzi.io/next-node-ids pin, and a single broker so the controller-vs-broker node-id collision could never occur. That is exactly the defect the hook exists to prevent, so the suite stayed green on a revision whose in-place migration lost every message — reproduced by hand 2/2 on a live cluster. A suite that cannot fail on this code is worse than no suite: it reads as migration coverage and invites trust it has not earned, while costing ~380s on every Kafka PR and asserting only that a PVC object exists, never reading a record back. The migration hook's bats suite and the live upgrade runs are the real coverage; the Strimzi bump would delete this suite regardless. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
1c88014661 |
fix(installer): keep node_modules out of the packages artifact (#4290)
## What this PR does `flux push artifact` walks the whole `packages/` tree and reads neither `.gitignore` nor `.sourceignore`, so a working copy where somebody ran `pnpm install` under the vendored console ships the console's dependency tree inside the bundle. Helm then refuses the dashboard chart on every cluster that pulls it, because one dependency file sits above the 5MB per-file limit, and the build itself reports nothing: ```text Could not load chart: decompressed chart file "dashboard/images/console/node_modules/.pnpm/@rolldown+binding-darwin-arm64@1.0.0-rc.15/node_modules/@rolldown/binding-darwin-arm64/rolldown-binding.darwin-arm64.node" is larger than the maximum file size 5242880 ``` Passing `--ignore-paths` replaces flux's built-in list rather than extending it, so the defaults are restated alongside `node_modules/`. `hack/check-packages-artifact-ignores.bats` holds both halves: dropping `node_modules/`, dropping one of the defaults, or declaring the variable without passing it to the recipe each turn the test red. Its second case re-reads the defaults out of the installed flux, so an upstream addition surfaces as a failure rather than as a pattern quietly missing from the artifact. Measured on a tree with the console's dependencies installed: the artifact goes from 80MB carrying 32397 `node_modules` entries to 6.7MB carrying none, with the dashboard chart still in it. ### Screenshots Not a UI change. ### Downstream repositories - [x] No downstream repository is affected by this change The two workflows that push the same artifact themselves (`e2e-fork.yaml`, `nightly.yaml`) run from a clean CI checkout, where the tree has no `node_modules` to pick up. ### Release note ```release-note fix(installer): the packages OCI artifact no longer picks up node_modules from a developer's working copy, which made the dashboard chart unloadable on every cluster that pulled the bundle. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Package artifacts now exclude unnecessary files, including version-control metadata, common media and archive formats, and dependency directories. * This helps prevent oversized files from exceeding Helm’s per-file size limit. * **Tests** * Added validation to ensure package artifact ignore patterns remain complete and aligned with the packaging tool’s defaults. * Optional checks report when the packaging tool is unavailable without failing the test suite. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
b18281ef0c |
feat(dashboard): serial console for virtual machines, and paste into the VNC console (#4356)
## What this PR does Adds a serial console tab to the VM detail page, and makes the VNC console accept a paste. Moving text into a VM was the starting point, and the two consoles answer it very differently. The VNC console carries pixels and key events and nothing else, so a paste there has to be typed, one emulated key press per character. The serial console carries text, so a paste is a single write and the guest's output is real text the user can select and copy. Measured on a running VM, 1000 characters, checked by piping the pasted string through `md5sum` in the guest and comparing with the digest computed locally: | | time | integrity | | --- | --- | --- | | serial console | **0.87 s** | digest matched | | VNC, 25 ms per key event | 56.6 s | digest matched | | VNC, no pacing | 10.3 s | **characters dropped and reordered** | The VNC rows were taken by driving the typing loop directly rather than through a real keypress, so they measure the typing and not the shortcut. The pacing is the measured floor either way: with none, the guest loses and reorders characters. ### Serial console New tab backed by KubeVirt's `virtualmachineinstances/console` subresource and xterm.js. A VM's tabs now lead with its two consoles — serial first, labelled Console, with VNC beside it — because reaching a console is why people open a VM. VNC keeps its place: the serial port does not reach a desktop session, a bootloader, or a Windows guest, and Windows guests are supported here, down to their own scheduling in the VM charts. Copy stays on the terminal's own chord, Ctrl+Shift+C or Cmd+C, so a plain Ctrl+C still interrupts the foreground process. Paste needed one thing handled, and only off a Mac: xterm turns Ctrl with a letter into a control byte and cancels the keydown, so Ctrl+V never reached the browser's paste and the guest received SYN (0x16) instead. Reproduced on a real terminal, where the shortcut put a single `0x16` on the wire and nothing else. The key handler now returns that chord to the browser, at the cost of the terminal's quoted-insert. On a Mac the browser pastes on Cmd+V, so there the terminal keeps the key. One detail worth knowing before touching this code: virt-api negotiates the `plain.kubevirt.io` subprotocol and reads **binary frames only**. A text frame is accepted by the socket and then silently dropped, which is indistinguishable from a dead console. That is why the stream is a small module of its own with its own tests, rather than xterm's stock attach addon, which sends text. ### Paste in the VNC console The graphical console still matters where the serial port cannot reach — a desktop session, a bootloader, Windows — so a paste works there too, typed key by key. **The shortcut is the platform's own: Cmd+V on macOS, Ctrl+V everywhere else.** That means Ctrl+V no longer reaches the guest on Linux and Windows, where it is visual block in vim or the guest's own clipboard. Taking both chords bought nothing, because no browser pastes on Ctrl+V on a Mac, and cost the guest a key it has its own use for. Nothing is added to the interface: the toolbar's existing status line reports `Pasting 12/270` while the text goes in, then returns to the connection state. Four constraints shaped the rest, each found by running it rather than by reading: - `navigator.clipboard.readText()` puts a permission prompt in front of **every** paste. Instead the shortcut moves the focus to a hidden field and lets the browser paste into it, which costs nothing and asks nothing. - noVNC calls `preventDefault()` on keydown. Letting the shortcut reach it would cancel the browser's paste outright, so the shortcut is taken on the capture phase and only its propagation is stopped. If no paste follows, the focus returns to the console instead of being parked in an invisible box. - The paste starts while the user is still holding the chord, and noVNC told the guest that modifier is down the moment it was pressed. Typing into that state sends chords rather than characters — under Ctrl an `m` is Return — so the host's modifiers are released before the first character. On Windows noVNC sits on the first Ctrl for 100 ms and delivers it on a timer, after that release has already happened, so the release is sent once more when the pacing has covered that window. - A character the keymap cannot produce changes what the rest means, and the newline after it still submits: `rm -rf /srv/данные` would become `rm -rf /srv`. Nothing is sent unless the whole paste can be typed, and the toolbar says how many characters were in the way. The keymap covers US only. A guest in another layout is the case the serial console answers, where text travels as bytes and no layout is involved. ### Screenshots The serial console on a running VM: the tabs lead with it, the terminal is live, and the middle line was pasted from the browser clipboard. <img width="2252" height="1606" alt="cozystack-serial-console" src="https://github.com/user-attachments/assets/6fac5f95-c142-405b-9b54-8ed040d65167" /> ### Downstream repositories - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: <!-- The diff touches only packages/system/dashboard/images/console. Walked the trigger map in docs/agents/contributing.md file by file: no entry matches a console-only change, and the map states outright that UI changes belong in this repository, in the same PR. --> ### Notes for the reviewer - New dependencies: `@xterm/xterm` and `@xterm/addon-fit`. The tab is split at the route with `React.lazy`, the way the YAML editor already is, so both land in a 347 KB chunk fetched when a console is opened. Built both revisions with the same `node_modules`: the entry chunk every dashboard page loads goes 935.91 KB → 896.39 KB, so it ends up smaller than before this branch. - The serial console needs a getty on `ttyS0` in the guest. Cloud images enable it by default; a Windows guest or a graphical desktop is the VNC console's job. - The serial console opens with the user's own identity — nginx forwards the session's `Authorization` untouched — and `virtualmachineinstances/console` is already granted next to `/vnc` in `cozy:tenant:use:base`, so no RBAC change is owed. - Verified against a live VM on a dev cluster: connection, typing, a 1000-character paste round-tripped through `md5sum` on the serial console, text typed before the serial socket opens (held and flushed), a real Ctrl+V on the VNC console arriving intact, and two pastes in quick succession (the second is refused rather than interleaved). - The Windows modifier path is read from the pinned noVNC in `node_modules`, not run: there is no Windows host here. The test fake models that delivery, so the behaviour is at least held by a test. - There is no cancel affordance for a paste in progress. It stops on its own when the session drops or the tab is left, and the toolbar then says how far it got; Reconnect is the manual way out. A length cap is worth adding separately — at 25 ms per character a large clipboard types for a long time. - 418 tests pass; build and typecheck are clean. `eslint .` reports 54 pre-existing problems across twenty files, none of them in the files this PR touches. ### Release note ```release-note feat(dashboard): virtual machines gain a serial console tab, where pasting is instant and the output can be selected and copied. The VNC console now pastes the host clipboard on the platform's own shortcut — Cmd+V on macOS, Ctrl+V elsewhere — typing it into the guest on a US keyboard layout; Ctrl+V no longer reaches the guest on Linux and Windows. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a serial console for running virtual machines, including terminal input, output, resizing, reconnection, and connection status. - Added clipboard paste support to the VNC console with keyboard-layout handling, progress feedback, and unsupported-character reporting. - Added platform-aware paste shortcuts for macOS and Windows/Linux. - **Improvements** - Console content now loads on demand with a loading indicator. - Added handling for stopped and unsupported virtual machine states. - **Documentation** - Documented keyboard, clipboard, and console testing behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
938595afe3 |
fix(keycloak): validate the realm list wherever it is set
The list was read only inside the adminHost branch, so a value the chart would refuse — master, a scalar, a name that splits the path — sat in the values unnoticed until someone turned the admin hostname on, and the refusal then landed on the change of authentication topology rather than on the edit that caused it. Build the list on every render and let the branches use the result. Assisted-By: LLM Signed-off-by: Kirill Ilin <stitch14@yandex.ru> |
||
|
|
13a4029b8b |
fix(keycloak): count the realms the route budget rejects
The over-budget message derived the realm count from the path list, which also carries the two static paths, so it named one realm more than the deployment has — the one number an operator acts on. Keep the realm list in its own template and count it there, and derive the budget from the static paths rather than restating 31. The shape guard also let a falsy scalar through, so `exposedRealms: false` reached range and failed with a Go template error instead of the message that says what to write. Assisted-By: LLM Signed-off-by: Kirill Ilin <stitch14@yandex.ru> |
||
|
|
714ced9bab |
fix(keycloak): refuse more realms than a route rule can match
A Gateway API route rule takes 64 path matches and each published realm takes two of them, so past 31 realms the apiserver rejects the whole HTTPRoute and the login hostname stops answering. Fail while rendering, naming the parameter to shorten, instead of leaving the limit in a comment. The Ingress path has no such cap, so the check lives with the route. Assisted-By: LLM Signed-off-by: Kirill Ilin <stitch14@yandex.ru> |
||
|
|
c96fe3d3ef |
fix(platform): refuse master as the OIDC realm name
The realm named here is where keycloak-configure creates every platform client and the cluster-admin role, and where dashboard users sign in. Naming master puts all of that inside the realm that holds the Keycloak super-admin, and publishes its login page wherever the platform realm is published — and the guard belongs where the value is set, since the Keycloak chart only reads it once a separate admin hostname is configured. Assisted-By: LLM Signed-off-by: Kirill Ilin <stitch14@yandex.ru> |
||
|
|
e87232b822 |
fix(keycloak): always publish the platform realm and never master
exposedRealms replaced the published realm instead of adding to it, so a deployment naming only its back-office realm took its own login off the public route: every component's issuer URL points at the platform realm, and they would all have met a 404. Publish the platform realm unconditionally and deduplicate, which is also what the parameter's description promised. Refuse master in either position rather than warning about it in a comment — publishing it is exactly what the realm enumeration exists to prevent. Assisted-By: LLM Signed-off-by: Kirill Ilin <stitch14@yandex.ru> |
||
|
|
0b8296d689 |
fix(dashboard): release the held modifier before the first character, not after some
The window this had been measuring started before the paste did. noVNC arms its timer on the keydown, and the modifier is delivered once — by that timer or by the next keyup, whichever comes first, and the V keyup is not intercepted. Both land within 100 ms of the keydown, which at the default pace is somewhere inside the opening releases. Counting from the end of those releases therefore started the clock again and let three characters through while the guest still held Ctrl, where an m is Return. The second release now waits out whatever is left of the window, measured from the keydown itself, and happens before any character goes in. The test that was written for this pinned the wrong placement: it handed the modifier over after the first character, later than noVNC can, and the fake's method was the immediate one under a second name. The fake now delivers on its own schedule, partway through the opening releases, so a test cannot choose the moment. The component's own typing pace had no test at all once every existing one passed its own value, so setting the default to zero left the suite green. One test renders without the prop and holds it. The lifecycle tests key on characters typed rather than on a count of sleeps, which is what let them drift past their code when the opening releases changed shape. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
64229b154e |
fix(keycloak): bound each published realm path to a path segment
ingress-nginx renders `pathType: Prefix` as a plain nginx prefix location instead of the element-wise match the Ingress API specifies, so a lone /realms/cozy also served /realms/cozy-admin — a realm the chart claims is unpublished. Publish each realm as an exact path plus a trailing-slash prefix, which bounds the match on every controller and on Gateway API. While here, reject a scalar in place of the realm list instead of failing inside range, and point a malformed platform realm name at the parameter that carries it rather than at exposedRealms. Assisted-By: LLM Signed-off-by: Kirill Ilin <stitch14@yandex.ru> |
||
|
|
5149e67dd7 |
feat(keycloak): publish only the named realms on the login host
The narrowed login route carried a bare /realms prefix, so it also published realm master: its token endpoint accepted super-admin credentials from the internet. Enumerate the published realms instead — the platform realm by default, ingress.exposedRealms when a deployment serves more than one. Behavior changes only where ingress.adminHost is already set. Assisted-By: LLM Signed-off-by: Kirill Ilin <stitch14@yandex.ru> |
||
|
|
cd91627c78 |
feat(marketplace): register a tapped repository's apps on connect (#4308)
## Problem Connecting a marketplace repository materialized its `PackageSource` but created no `Package`, so its `ApplicationDefinition`s never rendered and its apps never appeared in the dashboard catalog until a manual `cozypkg add`. The dashboard cannot drive that step: the package list it shows is computed from the `ApplicationDefinition`s, which do not exist yet, a chicken-and-egg that leaves a connected repository unbrowsable. ## Fix The tap materializer creates a tap-managed registration `Package` for each materialized `PackageSource` that has a `default` variant, the same way the platform ships a `Package` per built-in app. Its install-marked components (the `ApplicationDefinition` registrations) deploy on connect, so the apps become browsable; the app components carry no `install` block and are still instantiated per user resource, so registration deploys no user workload. Privileged install components are not installed without confirmation. The check is at the install site: the Package reconciler skips a privileged install component of a Package that still carries the marketplace-tap label (a tap auto-registration the operator has not confirmed) and reports `Ready=False PrivilegedNotConfirmed`, and `resolvePrivilegedNamespaces` gates the PodSecurity label on the same predicate. `cozypkg add --allow-privileged` sheds the label (an explicit ownership handover), which is the confirmation; a platform Package never carries the label. Putting the check where the install happens closes the window a materializer-side guard would race, so the earlier install-safety de-register machinery is removed. Two further best-effort layers close the paths a lingering artifact or release could take when a source revision flips a previously benign component to privileged: `cleanupOrphanedHelmReleases` treats an unconfirmed privileged component as not-desired and removes a HelmRelease left from an earlier benign revision, and the PackageSource reconciler withholds a privileged component's `ExternalArtifact` until confirmed (re-running on confirmation through a Package watch), so the helm-controller has no privileged content to upgrade to. A component flipped to privileged in a new revision can still be materialized by source-watcher before any reconciler observes the flip; closing that race fully requires decoupling the catalog registration from install, tracked in #4359. `cozypkg untap` and the dashboard disconnect remove the registration `Package` too, so the apps de-register (its registration HelmReleases and their `ApplicationDefinition`s are garbage-collected); running instances are left in place but unmanaged until re-tapped, the same outcome as `cozypkg del`. Teardown deletes the Package under a UID + ResourceVersion precondition with a re-classify-on-conflict retry, so it never clobbers a Package a concurrent `cozypkg add` handover has just confirmed. `cozypkg add` re-opens a tap auto-registration for variant selection and hands it over to the user. ## Verification - The install-site privileged gate is covered by a Package-reconciler test (a managed registration with a privileged default variant creates no privileged HelmRelease and does not raise the namespace; a confirmed Package installs) and is mutation-verified. - The two best-effort layers each have a mutation-verified test: cleanup of a HelmRelease left when a benign component flips to privileged, and withholding of a privileged component's `ExternalArtifact` while the registration is unconfirmed (a non-tap platform source is never gated). - The `cozypkg add` handover is covered by a test asserting the tap label, the source annotation and the PackageSource ownerRef are shed and the variant pinned. - The teardown delete precondition is covered by a test in which a handover lands between the read and the delete; the precondition makes the delete conflict and the retry re-classifies and spares the user's Package. Earlier end-to-end on a dev cluster (predates the install-site refactor and the layers above) with a real public demo repository `oci://docker.io/999669/cozy-gitea:v1.0.0` confirmed the connect→auto-register→instance→disconnect flow: `cozypkg tap` alone registered the app within ~10s; a `Gitea` instance reached Ready and served `/api/healthz`; `cozypkg untap` de-registered it and left the running instance. etcd healthy, `cozystack-platform` untouched. ```release-note Tapping a marketplace repository now registers its applications in the catalog on connect, without a manual `cozypkg add`. ``` |
||
|
|
92b8df076c |
fix(dashboard): a paste could still type chords on a Windows host
Windows is the platform where noVNC holds the modifier back: the first Ctrl keydown tells the guest nothing, arms AltGr detection and starts a 100 ms timer, and Ctrl reaches the guest when that timer fires. Taking the shortcut on the capture phase hides the V from noVNC, so the timer is what delivers — after the paste released a modifier the guest had not yet been told about. Everything typed between the two goes in as chords. The release is now sent a second time, once the pacing has covered that window. It happens once rather than per character, and it leaves Shift alone, because that one is held deliberately for capitals. Two tests had drifted past the code they are named for. Both dropped the session on the first sleep, which since the prelude lands before anything is pressed, so they held whatever the typing loop did: removing the session check from the loop left them green. They drop it after a character now, and a third covers the closing release, which no test reached at all. The fake learns the Windows path it could not express, and decodes function keysyms: Enter is 0xff0d, and treating it as a Unicode offset produced a negative code point, so the first test to paste a newline would have thrown instead of failing an assertion. The typing pace is injectable, so tests no longer run at the speed of a real keyboard and stop crowding their neighbours out on timeouts. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
6209a0b8d3 |
refactor(dashboard): code-split the serial console at the route
The tab pulled xterm through a dynamic import inside its own effect, which split the chunk but left the stylesheet on a fragile path: the dev server failed to fetch it once after a dependency rebuild, and the tab came up with the terminal missing. Splitting at the route instead is what the YAML editor next door already does. The imports go back to plain ones, React.lazy gives the tab its own chunk, and the stylesheet travels with it rather than through an import() of a CSS file. The entry chunk every dashboard page loads ends up smaller than before this branch: 935.91 KB at the merge base, 895.99 KB here, with the tab and xterm in a 347 KB chunk fetched when a console is opened. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
4a10497d3d |
fix(dashboard): a VNC paste typed chords, and could run a command nobody pasted
Three defects on the same surface, all of them where this code meets noVNC, the platform and the guest — and all of them invisible to every test here, because the fake had no keyboard. A paste began while the user was still holding the chord. noVNC tells the guest a modifier is down the moment it is pressed and releases it only on keyup, so the pasted text arrived as chords: under Ctrl an m is Return, and on macOS, where noVNC maps Cmd to Alt, the same under Alt. The host's modifiers are now released before the first character. A character the keymap cannot produce was dropped and the rest was typed anyway, newline included, with the notice arriving after the line had run: rm -rf /srv/данные became rm -rf /srv. Nothing is sent now unless the whole paste can be typed. The shortcut was taken on every platform, so the guest lost a key it has its own use for — visual block in vim, its own clipboard on Windows — and on macOS gained nothing, because no browser pastes on Ctrl+V there. Each platform's own chord only. The fake now models the guest's keyboard: which modifiers it believes are down, and what it received with chords marked. That is what makes the first defect expressible as a test rather than something only a person at a keyboard could notice. Also pins three behaviours the suite was not holding: a frame arriving after cleanup, a real close reason behind a synthetic 1006, and the 25 ms pacing the description calls measured. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
d113d02e03 |
feat(dashboard): lead a VM's tabs with its consoles, serial first
Reaching a console is why people open a virtual machine, so the two consoles now sit directly after Overview instead of below the resource tabs. Serial leads and is labelled Console: text goes in and out of it directly, at a speed the emulated-keyboard path cannot reach. VNC keeps its place beside it, because the serial port does not reach a desktop session, a bootloader, or a Windows guest — and Windows guests are a supported case here, with their own scheduling in the VM charts. The order is pinned by a test: it was ordinary prose in a push() call before, so the next edit to this list could reorder it in silence. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
25cd86c560 |
fix(dashboard): Ctrl+V pasted nothing in the serial console
xterm turns Ctrl with a letter into a control byte and cancels the keydown, so on Windows and Linux Ctrl+V never reached the browser's paste and the guest received SYN (0x16) instead. Cmd+V and Shift+Insert were unaffected, which is why testing on a Mac missed it: reproduced on a real terminal, where the shortcut put a single 0x16 byte on the wire and nothing else. The key handler now returns the event to the browser, at the cost of the terminal's quoted-insert on that chord — the same trade the VNC console already makes for the same shortcut. The test stub keeps the handler now instead of dropping it, so the chord is reachable from a test at all. Three smaller repairs in the same paths: A frame arriving after cleanup was written into a disposed terminal, because the message handler never consulted the stream's own state. onerror synthesised a 1006 and the real onclose behind it was swallowed by the already-closed latch, so a close carrying 1008 Forbidden reached the user as connection error. The error is recorded and the close reports what the server actually said. navigator.clipboard is SecureContext-only and absent over plain http, which the console's own URL builder supports; copying threw there instead of falling back to the selection the user can copy by hand. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
58d0c90e0b |
test(dashboard): pin the console behaviours nothing was holding
Three behaviours the code promises had no test: the refusal of a second paste while one is still typing, the focus-restore timer, and the serial console's copy chord. The chord was untestable by construction — the Terminal stub accepted the handler and dropped it, so nothing could ever call it. The stub keeps it now. Each test was checked by reverting the behaviour it covers and watching that one test, and only that one, go red. The VNC paste also acts on how typeKeystrokes ended instead of discarding it. A paste cut short by a dropped session used to leave the toolbar reading as though the text had all arrived. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
71bdad69ae |
fix(dashboard): console input state across a session's lifetime
Five repairs, all on state that outlives a single interaction. The serial stream marked itself writable before the socket opened, so a keystroke typed while the pill still said Connecting reached WebSocket.send() during CONNECTING, which throws. Input is now held and flushed on open, and discarded if the socket closes first. A paste in the VNC console outlived the tab. The effect cleanup dropped the RFB reference but not the sender, and noVNC's disconnect event arrives too late to clear it, so the typing loop kept running against a dead session. Cleanup now aborts the paste and clears the sender. Clipboard blocked had a renderer and no producer: the state survived the move from the panel to the shortcut, while the only thing that used to set it did not. The sink timeout sets it now, which is the case the message was written for — an empty or refused clipboard raises no paste event at all. The shortcut matched the physical KeyV position only, so on Dvorak Ctrl+V reached noVNC instead and was cancelled there. A clean server-side close left the status pill on amber Connecting, because the tab tracked a boolean where it needed three states. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
3d026ff611 |
refactor(dashboard): keep only the US keymap for VNC paste
The layout was never selectable — the tab picks one and nothing offers the others — so the German, French and Russian tables, and the AltGr handling that only they needed, were data and code with no reader. What remains states its assumption plainly: US, the layout a default cloud image boots with. A guest in another layout is the case the serial console answers, where text travels as bytes and no layout is involved. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
2f6ec92a20 |
feat(dashboard): paste into the VNC console with Ctrl+V
The graphical console still matters for guests the serial port cannot reach — a desktop session, a bootloader, Windows. There a paste has to be typed, so this maps the clipboard text to key presses for the layout the guest has active and paces them: without the pacing the guest drops and reorders characters, which was measured rather than assumed. The shortcut is taken before noVNC sees it. noVNC calls preventDefault() on keydown, which would cancel the browser's own paste and hand the guest a Ctrl+V it cannot use; instead the focus moves to a hidden field and the browser pastes into it. Reading the clipboard through navigator.clipboard would have put a permission prompt in front of every paste. The focus returns to the console even when no paste follows. Nothing is added to the interface: the toolbar's existing status line reports progress while the text is typed. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
27d7b0ee03 |
feat(dashboard): serial console tab for virtual machines
The VNC console carries pixels and key events, so moving text through it means emulating a keypress per character: measured at 57 seconds for a 1000-character paste, and the text corrupts if the pacing is dropped. KubeVirt also exposes the guest's serial port as a websocket, and that one carries text. The same 1000 characters land in under a second, the guest's output is real text the user can select and copy, and tabs, newlines and non-Latin characters travel as bytes instead of depending on the keyboard layout the guest happens to have active. virt-api negotiates the plain.kubevirt.io subprotocol and reads binary frames only — a text frame is accepted by the socket and then dropped, which is indistinguishable from a dead console, so the stream is kept deliberately narrow and tested on its own. Copy stays on the terminal's own chord (Ctrl+Shift+C, or Cmd+C) so a plain Ctrl+C still interrupts the foreground process. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |
||
|
|
aa20495df3 |
feat(marketplace): register a tapped repository's apps on connect
Connecting a marketplace repository materialized its PackageSource but created no Package, so its ApplicationDefinitions never rendered and its apps never appeared in the dashboard catalog until a manual `cozypkg add` the dashboard cannot drive (the package list is computed from the ApplicationDefinitions, which do not exist yet). The tap materializer now creates a tap-managed registration Package for each materialized PackageSource, the way the platform ships a Package per built-in app, so the ApplicationDefinition registrations deploy on connect and the apps become browsable; the app components carry no install block and are still instantiated per user resource. Privileged install components of an unconfirmed tap auto-registration (a Package that still carries the marketplace-tap label) are refused on several best-effort layers: the Package reconciler skips their HelmRelease and prunes a lingering one at the install site, and the PackageSource reconciler withholds their ExternalArtifact so the helm-controller has no privileged content to deploy, re-running on confirmation via a Package watch. `cozypkg add --allow-privileged` sheds the label as an explicit ownership handover, which is the confirmation; a platform Package never carries the label. A benign component flipped to privileged in a new revision can still be materialized by source-watcher before these layers observe the flip; closing that race fully requires decoupling the catalog registration from install and is tracked in issue #4359. `cozypkg untap` and the dashboard disconnect remove the registration Package (de-registering the apps) under a UID + ResourceVersion precondition, so a concurrent `cozypkg add` handover is never clobbered; running instances are left but unmanaged. `cozypkg add` re-opens a tap auto-registration for variant selection and hands it over. The cozystack-api ClusterRole gains delete on packages for the disconnect path. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com> |