mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-06 20:13:43 +09:00
feat/cozystack-controller-gpu-only
111
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca74e5b750 |
feat(vm-instance): attach GPUDevice objects through device and deviceTemplate
A gpus entry can now reference a held GPUDevice by name or ask for a device of a class for as long as the VM runs, next to the existing raw resource name. References are rendered only as the device-reference label and one JSON list in the devices annotation on the VMI template. The GPU webhook resolves them when the VMI is created, and the label is what selects the VMI and its virt-launcher pod for it. The schema cannot say "exactly one of", so name becomes optional and the template refuses an entry that sets none or several of the three, a list mixing raw names with references, a device listed twice, and a deviceTemplate without a class or with a count below one. A zero deviceTemplate counts as unset, since a typed client sends one on every entry. The dashboard offers the tenant's devices and the GPU catalog through the gpudevice and gpuclass option sources. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
e03e14aa14 |
feat(platform): thread the GPU switch and settings to the controller
cozystack-controller runs on every cluster, and its GPU paths watch KubeVirt objects, so the chart's gpu.enabled is set only where the GPU admission policies apply: where gpu-operator serves GPUs to VMs. monitoring-agents scrapes the metrics those paths serve under the same condition. gpu.resourceNames and gpu.rawResourceNames reach the controller, which publishes them in its settings ConfigMap for the policies and cozystack-api. rawResourceNames also reaches cozystack-basics, whose raw-name policy renders only under forbidden. Both are validated on every render, so a typo fails before anyone turns GPUs on. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
b9c1d465aa |
feat(platform): run the GPU node agent with gpu-operator
GPUNode inventory is needed exactly where GPUs are served, so the agent ships as a component of every gpu-operator variant, in the cozy-gpu-operator namespace. It is marked privileged because its hostPath mounts are refused below the privileged Pod Security level, and the operator labels a namespace privileged only for privileged components. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
7045899800 |
feat(platform): gate the GPU policies on gpu-operator, add gpu.vgpu
The GPU class and device admission policies in cozystack-basics apply only where gpu-operator serves GPUs to VMs: emitted by the iaas bundle in the default or vgpu variant. A helper reads the same bundle keys as the iaas bundle's own gpu-operator gate to decide that. gpu.vgpu entitles the root tenant through _cluster.gpu-vgpu, written only when on because every release in every tenant reads that Secret. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
1f74ad4ee7 |
fix(multus): drop the CPU limit and make resources configurable (#4830)
<!-- 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 The multus daemon was too small to recover a node after a CNI outage, and its limits could not be raised from values. The 100m CPU limit and the 1Gi memory limit of `kube-multus` were hardcoded in the patched upstream template. On a node with about 600 dead sandboxes and 220 pods waiting, multus was throttled in every CFS period. Its calls took longer than kubelet's two-minute CRI deadline, so every call was retried and the queue only grew. With the CPU limit lifted it OOM-looped at 1Gi. With 2 CPU and 4Gi it cleared the backlog in about three minutes. Now the resources come from a new `resources` value. The default keeps the same requests (100m CPU, 100Mi memory), has no CPU limit, and raises the memory limit to 4Gi. The memory limit stays on purpose. Without it the pod cgroup has no `memory.max`, and multus becomes a candidate for the Talos userspace OOM handler again. The chart now refuses to render when an override removes the memory limit or the memory request. Without a request Kubernetes uses the limit as the request, so a node with less allocatable memory than the limit would be left without multus. The change is in `patches/customize-deployment.patch`, so `make update` keeps it. I applied the patch to upstream v4.3.0 and got the committed template, except the image references. The platform reverts a direct edit of the child `cozystack.multus` Package, so a running cluster sets the value through the new `networking.multus.resources` key of the platform Package. The key is passed on only when set, and a value that is not a map stops the render. The system-defaults LimitRange in the namespace sets memory only, so it does not bring a CPU limit back. Closes #4829 ### Screenshots <!-- REQUIRED for UI changes: attach screenshots or screen recordings demonstrating the visual impact of your changes. PRs with UI changes without screenshots will not be merged. --> 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. --> - [ ] No downstream repository is affected by this change - [x] [cozystack/website](https://github.com/cozystack/website) - follow-up: https://github.com/cozystack/website/pull/747 - [ ] [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 adds a platform value, so the hand-written key table on the website needs it. The follow-up PR there is a draft until this one merges. ### 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(multus): the kube-multus container no longer has a CPU limit, and its memory limit is raised from 1Gi to 4Gi, so a node with hundreds of pods can recover after a CNI outage. Its resources can be overridden through the new `networking.multus.resources` platform value. An override that removes the memory limit or the memory request now fails the render. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added optional CNI plugin staging during Multus startup, configurable through chart values. Staging skips when the plugin directory is unavailable and reports individual copy failures while continuing. * Made Multus resource settings configurable, with defaults of 100m CPU and 100Mi memory requests and a 4Gi memory limit. Rendering requires memory requests and limits. * Updated the Multus deployment to use the `cozy-multus` namespace and DaemonSet name, and Cilium as the master CNI configuration. * **Bug Fixes** * Added a 128Mi memory limit to the CNI installation step. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
938abdc4d2 |
fix(platform)!: refuse a networking variant change on a live cluster (#4790)
## What this PR does Changing the platform variant on a running cluster can delete its CNI, and nothing stopped that before. A new platform variant (`spec.variant` of the `cozystack.cozystack-platform` Package) can give the `cozystack.networking` Package a different variant, and the operator then deletes every HelmRelease of that Package the new variant does not install. Moving from `isp-full` to `isp-hosted` deletes Cilium and Kube-OVN, so every pod loses networking. Moving from `isp-full` to `isp-slim` deletes Kube-OVN. With this PR the platform render looks up the live `cozystack.networking` Package. If its variant differs from the one about to be rendered, the render fails, and the error names the value that lets it through: `networking.acknowledgeVariantChange: "<new variant>"`. I made the acknowledgement a variant name instead of a boolean like `publishing.proxyProtocolAcknowledgeUnclean`. A leftover `true` would let every later switch through unchecked, while a variant name covers only the switch it names. The guard does nothing when there is no live Package to compare against: `helm template`, a client-side dry-run, or a first install. It is also skipped when `cozystack.networking` is listed in `bundles.disabledPackages`, because the platform does not render the Package then. It lives in the shared Package helper and not in `bundles/system.yaml`, so it covers both places that emit the networking Package, including the `noop` call in the `isp-hosted` branch. The other Packages whose variant follows the bundle, `cozystack-scheduler` and `tenant-application`, keep the same release names in every variant, so changing their variant deletes nothing and they need no guard. The new helm-unittest cases cover the refused switches, an unchanged variant, a matching and a mismatching acknowledgement, a bare boolean, `disabledPackages`, an offline render, and a live cluster with no networking Package yet. A cluster where someone set the Helm-owned networking Package to `cilium-kilo` by hand now fails its platform upgrade, and the error points it at `bundles.disabledPackages`. Acknowledging does not help there, because the render writes the bundle's variant over `cilium-kilo`. helm-controller v1.5.0 installs a release with server-side apply by default, which takes over every rendered field. On a release it installed, the upgrade replaces the variant and the operator deletes Kilo. A release first installed with Helm 3 keeps the apply method recorded in its history, so there the outcome depends on that history. Known gap, left for a follow-up: the guard fires on any variant change, including Talos to generic (`kubeovn-cilium` to `kubeovn-cilium-generic`). Those pairs share release names, so nothing is deleted and only values change. Refusing is safe, and the error does not claim a deletion for such a switch. Closes #4596 ### Screenshots <!-- REQUIRED for UI changes: attach screenshots or screen recordings demonstrating the visual impact of your changes. PRs with UI changes without screenshots will not be merged. --> ### Downstream repositories This PR adds a key to `packages/core/platform/values.yaml`, and the website keeps a hand-written table of those keys in `operations/configuration/platform-package.md`. - [ ] No downstream repository is affected by this change - [x] [cozystack/website](https://github.com/cozystack/website) - follow-up: https://github.com/cozystack/website/issues/746 - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note fix(platform): the platform render now refuses to change the variant of a live cozystack.networking Package, because the operator would then delete the CNI HelmReleases the new variant does not install (isp-full to isp-hosted removes Cilium and Kube-OVN). To go ahead with such a change, set networking.acknowledgeVariantChange to the new networking variant named in the error. A cluster whose cozystack.networking variant was set by hand (for example cilium-kilo) needs cozystack.networking in bundles.disabledPackages before the upgrade, otherwise the platform render refuses. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Changing the networking variant on a running cluster now requires explicit confirmation naming the requested variant. A change without a matching confirmation is blocked. * The confirmation applies only to the specified switch and must be updated for a later variant change. If the running package has no recorded variant, it is treated as using the default variant. * New installations without an existing networking package are unaffected, and networking can still be disabled through its package setting. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
ddd00f8639 |
feat(platform): pass multus resources from networking.multus.resources
The multus chart takes its resources from values, but the platform renders the cozystack.multus Package with stageCniPlugins alone, and an edit to that child Package is reverted by the platform reconcile. A running cluster therefore had no way to raise the multus limits. networking.multus.resources is passed to the Package only when set, so an unset key leaves the chart default in charge. A value that is not a map stops the render here; the multus chart keeps refusing a render without a memory limit or request. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
1245b32a32 |
fix(platform)!: refuse a networking variant change on a live cluster
Changing the variant of the cozystack.cozystack-platform Package on a running cluster changes bundles.system.variant and can re-render the cozystack.networking Package with another variant. The operator then deletes every HelmRelease of that Package the new variant does not install, so isp-full -> isp-hosted removes Cilium and Kube-OVN and the cluster loses its CNI; isp-full -> isp-slim removes Kube-OVN. The platform render now looks up the live Package and fails when its variant differs from the one about to be rendered, unless networking.acknowledgeVariantChange names the new variant. The acknowledgement is a variant name rather than a boolean so that a value left behind after one switch does not cover a later one. The check is inert where lookup returns nothing (helm template, client-side dry-run, first install) and skipped when cozystack.networking is in bundles.disabledPackages, since the platform then does not render it. A live variant set by hand, such as cilium-kilo, is pointed at bundles.disabledPackages rather than at the acknowledgement: the acknowledged render would write the bundle's variant over it. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
87a64071a7 |
fix(cilium)!: take cilium's own addresses from a block kube-ovn does not hand out (#4778)
## What this PR does
fixes #3740.
on the kube-ovn variants cilium allocates no pod ips, but the agent
still takes its router ip (`cilium_host`, `CiliumInternalIP`) and the
gateway api ingress ip from its own allocation cidr. with `ipam.mode:
kubernetes` that's `Node.spec.podCIDR`, a /24 out of the same /16
kube-ovn hands pods from, so sooner or later kube-ovn gives a pod an
address cilium already holds. we hit it on three clusters, details in
https://github.com/cozystack/cozystack/issues/3740#issuecomment-6022394243.
**what changes**
- the kube-ovn variants run cilium with `ipam.mode: cluster-pool`, a /29
per node out of the last /21 of `networking.podCIDR` (`10.244.248.0/21`
for the default /16). the platform derives the block from
`networking.podCIDR` and passes the same range to kube-ovn as
`--default-exclude-ips`, so a fresh install never hands it to pods.
- the block sits inside the pod cidr on purpose. envoy reaches gateway
api backends from the node's ingress ip, and kube-ovn masquerades host
originated traffic only when the source is in `ovn40subnets`. with the
`100.65.0.0/16` pool from the issue a nodeport through the gateway
answered 200 only on the backend's node and 503 `upstream connect error
... connection timeout` on the other two. with the block inside the pod
cidr it's 200 from all three. worth a look on the ci side too. the ci
sandbox sets talos `podSubnets` to `100.65.0.0/16`, so cilium's ingress
ips there already come from outside `ovn40subnets`, and the gateway e2e
checks Accepted, Programmed and the lb service but sends no request
through a listener, so it wouldn't notice. with this pr the sandbox
takes its block from the pod cidr like everyone else.
- the end of the /16 is what kube-ovn reaches last. it hands out the
lowest free address and reuses released ones only when the free range
runs dry (`pkg/ipam/subnet.go` and `pkg/ipam/ip_range_list.go` at
v1.15.26). a /21 cut into /29s covers 256 nodes.
- the slim variants and guest clusters stay on `ipam.mode: kubernetes`.
cilium is the only cni there and `Node.spec.podCIDR` is the pod range.
- `networking.podCIDR` now needs /20 or wider on the kube-ovn variants,
otherwise the render fails and says why. an install that rendered before
can stop rendering at upgrade, hence the `!`. a /21 cut into /29s also
caps these variants at 256 nodes, a fresh install included.
**existing clusters**
a post-upgrade hook in the cilium chart moves them
(`templates/ipam-migration.yaml`, `files/ipam-migration.sh`). before it
touches anything it checks that the block has a /29 for every CiliumNode
and that it lies inside kube-ovn's default subnet, and stops with both
cidrs printed otherwise. a node cleared past the pool's capacity would
lose pod networking on its next agent restart, and a block outside the
subnet puts the agents outside `ovn40subnets` again. it has to be
post-upgrade. platform migrations run as the platform's pre-upgrade
hook, while every agent still runs `ipam.mode: kubernetes` and writes
`Node.spec.podCIDR` back into a cleared CiliumNode
(`pkg/nodediscovery/nodediscovery.go` at v1.19.5), so the move can only
happen after the cilium release switched the mode. the job runs on the
host network against the agent's own api endpoint (kubeprism on talos),
because it restarts the agents.
1. adds the block and every node's current ingress ip to `excludeIps` of
the default subnet, in one json patch with a `test` op, and reads the
subnet again on conflict, since kube-ovn rewrites that list into its own
order right after a patch. the old ingress ip goes in because cilium
1.19.5 keeps a former ingress ip in the agent's ipcache as
`reserved:ingress` after the move. we saw 2 to 3 such entries per node
hours later. old router ips leave the ipcache and go back to kube-ovn.
2. stops right there if an address in the block is still allocated,
names it, and moves no agent. it first checks the subnet status it
patched against, then waits until `status.v4availableIPrange` stops
offering the block, which is kube-ovn having applied the exclusion, and
then reads the `ips.kubeovn.io` objects of the default subnet. the
status alone can't answer it, kube-ovn leaves excluded addresses out of
`v4usingIPrange` (`GetSubnetIPRangeString` in `pkg/ipam/ipam.go` at
v1.15.26), so a pod still in the block would be invisible on a retry
where the exclusion is already in place. recreate those pods and the
next upgrade retry goes through.
3. one node at a time, clears the inherited /24 from
`spec.ipam.podCIDRs` (again with a `test` op, so a concurrent writer
fails the patch instead of losing an update), waits for the operator to
allocate a /29 from the block and restarts that node's agent. a node
that isn't ready is skipped and its agent picks up the block when it
starts. nodes already moved are left alone, so a retry after a timeout
just continues. the hook runs inside the cilium upgrade, so where
restarting every agent takes longer than the upgrade timeout, 10m by
default, the release first reports a failed upgrade and the move
finishes on the retry.
running pods keep their addresses. every agent restarts twice on
upgrade, once when the config change rolls the daemonset and once from
the hook, and while it restarts host to pod traffic on that node blips
for a few seconds because the router ip moves. a pod that sits on a
router ip today is reachable again after the move, the node no longer
holds that address. a pod on a node's current ingress ip keeps it and
that node keeps seeing it as `reserved:ingress`, so recreate it. the
hook logs those pods by name.
### Validation
- `helm unittest` passes cilium 23/23, platform 246/246 and kube-ovn
22/22. the new suites pin the rendered `cilium-config` keys per variant,
the hook shape, env and rbac, and the platform wiring for the talos and
generic branches, a custom pod cidr, host bits in the network address,
the refusals for a too small or non ipv4 cidr, and the slim variant.
- on a trial merge with main at
|
||
|
|
0a2bb5ef98 |
fix(cert-manager): enable the Gateway API solver in the host cert-manager (#4039)
## What this PR does With the platform Gateway enabled, the ClusterIssuers in `cert-manager-issuers` and the per-tenant Issuers rendered for `TenantGateway` carry an `http01.gatewayHTTPRoute` solver, but the controller refuses it: the vendored chart never turns Gateway API support on, so every HTTP-01 challenge behind the Gateway fails with `gateway api is not enabled` and no certificate is issued. This passes a `ControllerConfiguration` with `enableGatewayAPI: true` to the host cert-manager through the component values of its platform Package. The host ships the Gateway API CRDs on every variant and orders cert-manager after them, so the flag is safe there. It is deliberately not a default of the `system/cert-manager` chart. The same chart is installed into tenant Kubernetes clusters, where the CRDs are an opt-in addon, and cert-manager v1.20 exits at startup when Gateway API support is on and the CRDs are missing. The tenant HelmRelease in `packages/apps/kubernetes` already passes the same `ControllerConfiguration` when its `gatewayAPI` addon is enabled, so both clusters now opt in from the layer that knows whether the CRDs are there, and the chart itself stays neutral. The new tests pin both halves: the host Package carries the flag and the preconditions that make it safe, and the chart wires `config` through to the controller while shipping none by default. Closes #3725 ### Screenshots Not applicable, no UI changes. ### 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: ### Release note ```release-note fix(cert-manager): enable the Gateway API solver in the host cert-manager, so ACME HTTP-01 challenges succeed when the platform Gateway is enabled ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Supported system bundles can now use cert-manager’s Gateway API HTTPRoute solver to issue certificates. - Hosted deployments include the required Gateway API custom resource definitions, installed before cert-manager. - Gateway API solver support remains opt-in in environments that do not provide the required definitions, avoiding unintended changes to certificate management there. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
76b9e55102 |
fix(cert-manager): enable the Gateway API solver in the controller
With the platform Gateway enabled, the ClusterIssuers in cert-manager-issuers and the per-tenant Issuers rendered for TenantGateway carry an http01.gatewayHTTPRoute solver, but the controller only honours that solver when Gateway API support is switched on, and the vendored chart leaves it off. Every HTTP-01 challenge behind the Gateway then fails with "gateway api is not enabled" and no ACME certificate is issued. Pass a ControllerConfiguration with enableGatewayAPI to the host cert-manager through the component values of its platform Package. The host always ships the Gateway API CRDs and orders cert-manager after them, so the flag is safe on every variant. It is not a default of the system/cert-manager chart: the same chart is installed into tenant clusters, where the CRDs are an opt-in addon and a controller started with Gateway API support and no CRDs exits at startup. The tenant side keeps opting in on its own. Signed-off-by: Aleksei Sviridkin <f@lex.la> Assisted-by: LLM |
||
|
|
d61fed46cc |
feat(local-storage): add opt-in OpenEBS LocalPV hostpath package
LINSTOR needs the DRBD and ZFS kernel modules, so a cluster without them has no dynamic PV provisioning. OpenEBS LocalPV hostpath needs no kernel modules and also serves deliberately node-local workloads. The package is opt-in so it cannot change an existing cluster. Its class is named cozystack-local-hostpath so it cannot collide with an existing class name, and it is not the default so PVCs that omit storageClassName keep their current binding. The vendored RBAC is patched down to what volume reconciliation calls; helper Pods, Leases and analytics state stay in the install namespace. Images stay on docker.io, pinned by index digest. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
974aaa0bac |
test(hack): regenerate the selective install fixture
Two optional Cluster API providers, Proxmox and in-cluster IPAM, were added to the platform sources after the fixture was generated. The drift test compares the fixture with the current disable list, so it failed until they were added. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
f46a9797b8 |
fix(hack): keep platform services in selective installs
Dropping every package that no suite owns also drops services the rest of the platform relies on: multus for secondary networks, the cozystack and LINSTOR schedulers, flux-plunger, cozy-proxy and ouroboros, whose hairpin NAT the Kubernetes suites assert. Nothing waits for them, so no PackageSource edge reaches them, and adding one would also change reverse test selection. The remaining packages stay uninstalled on purpose, also when test selection answers with every suite: nothing in the run looks at them, so installing them only adds a readiness wait. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
59555c895e |
fix(hack): retain controllers and APIs for selective installs
Tenant HelmReleases need a shard reconciler, and the root SeaweedFS release creates COSI resources even when no backup suite is selected. Without those controllers, a fresh reduced install cannot become ready. The Kubernetes suites create worker pools through KubernetesNodes, and MongoDB renders an operator-owned CR. Selecting only the cluster API or application chart drops resources those suites require. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
361893e6f4 |
fix(hack): retain backup APIs in selective installs
Backup roundtrip suites create Bucket and backup resources that the application graph does not require. A graph-only closure therefore omits the APIs those suites need on a fresh install. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
610578be16 |
test(hack): verify selective install contract
A missing suite owner or a discarded discovery failure can leave a reduced install without the APIs its tests exercise. Graph-only checks cannot establish that the chart preserves the requested package set. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> |
||
|
|
dc362fa591 |
fix(cilium)!: take cilium's own addresses from a block kube-ovn does not hand out
On the kube-ovn variants Cilium allocates no pod addresses, but the agent still takes its router IP and the Gateway API Ingress IP from its allocation CIDR. In ipam.mode kubernetes that is Node.spec.podCIDR, which the documented installs slice from the same range kube-ovn allocates pods from, so kube-ovn sooner or later hands a pod an address Cilium already holds. Cilium now runs ipam.mode cluster-pool on the kube-ovn variants, with a /29 per node from the last /21 of networking.podCIDR, and kube-ovn excludes that /21 through --default-exclude-ips. The block stays inside the pod CIDR on purpose. Envoy reaches Gateway API backends from the node's Ingress IP, and kube-ovn masquerades host-originated traffic only when the source lies in one of its subnets, so a block outside them such as 100.65.0.0/16 breaks Gateway API traffic that crosses nodes. A post-upgrade hook moves existing clusters. It first checks that the block has a /29 for every node and lies inside kube-ovn's default subnet, and stops without changing anything otherwise. It adds the block and every node's current Ingress IP to excludeIps of that subnet, because the agent keeps a former Ingress IP in its ipcache as reserved:ingress. It then waits until kube-ovn stops offering the block and stops if an address in it is still allocated. kube-ovn leaves excluded addresses out of the subnet status, so that check reads its IP objects. Then, one node at a time, it clears the inherited /24 from the CiliumNode, waits for the operator to allocate from the block and restarts the agent. networking.podCIDR now needs a prefix of /20 or shorter on the kube-ovn variants, and the render fails otherwise. A /21 cut into /29s also caps these variants at 256 nodes. Assisted-by: LLM Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com> |
||
|
|
3497855e15 |
fix(platform): read the tracing rate only as plain decimal digits
Sprig's int64 reads a string with a leading zero as octal and turns a parse error into 0, so "0100" would reach the collectors as 64, and "08" or digits beyond int64 would leave the default in place without a word. A float beyond int64 converts differently on each CPU architecture, and the collector adds its replica count to the rate before splitting it. A string rate must now be plain decimal digits, and any rate above 1 TiB/s fails the render; the collector also falls back to the default on digits it cannot hold rather than start with a rate of zero, which the processor refuses. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
a0aea139fc |
fix(platform): refuse a malformed shared-central tracing rate
A rate written as a quantity or a fraction ("1Mi", "100KiB", 1.5) was
read as zero, so the platform wrote no key and every collector kept
the default without a word. The platform render now fails on anything
but a whole number of bytes per second. Helm reads a number from
values as a float64, which prints a million as 1e+06, so a whole
number is checked as a number and written as int64 digits.
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
|
||
|
|
f8deb8e171 |
fix(platform): write the tracing rate only when it is positive
apps.yaml wrote the rate for any non-zero value, so a negative one reached every tenant's cozystack-values Secret and upgraded every release in every tenant, while the collectors ignored it and kept the default. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
177b207a27 |
feat(monitoring): hold each shared-central tenant to a write rate
The design has the per-tenant collector rate-limit its tenant, so that a noisy or hostile tenant cannot exhaust a shared backend. The disk cap alone does not do that: VictoriaTraces always keeps the last two days, so a tenant writing faster than the cap allows over two days fills the disk for every tenant on the store. The platform sets the rate with monitoring.tracingCentralTenantBytesPerSecond, and it reaches every tenant in _cluster through the cozystack-values Secret. A change there is a change in the values each monitoring HelmRelease reads, so Flux upgrades them all; a rate read with lookup at render time would never reach a tenant whose release is already in sync. The platform writes the key only when it is set, since every release in every tenant reads that Secret. Without it the collector holds the tenant to the rate of a default 10Gi store. Each central tenant's collector splits the rate across its replicas and refuses data above it before the batch processor, so the sender sees the refusal. An OTLP/HTTP sender gets a 429 and retries; the limiter answers gRPC with RESOURCE_EXHAUSTED and no RetryInfo, which the OTLP specification makes final, so central tenants should export over HTTP. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
339be8d42d |
fix(vm-default-images,kubernetes-worker-image,vm-disk): wait for golden images to import and refuse cloning a failed one (#4392)
## What this PR does Both golden image catalogs, `vm-default-images` and `kubernetes-worker-image`, went Ready before any image had imported. Each one renders a DataVolume per image with immediate bind, so every import starts at install, but neither PackageSource had `healthCheckExprs`. The HelmRelease went Ready as soon as helm applied the DataVolumes, and an image that failed or stalled stayed green. This PR makes both releases wait for the imports. Each component's `install:` block now has a DataVolume health check. `current` holds only on `Succeeded`, and `failed` holds on `Failed`. Any other phase, `Unknown` included, matches neither, and flux keeps waiting. A DataVolume with no `status` key makes the CEL evaluation error out. Flux reports that as Unknown and keeps waiting too. The other settled phases are safe to treat as not ready for these charts. `PendingPopulation` and `WaitForFirstConsumer` are where a volume waits for a consumer, and immediate bind removes that wait. `UploadReady` and `Paused` belong to upload and multi-stage sources, and both charts render only `source.http`. There is no per-component timeout, so I did not set one. The window is the operator-wide `--helmrelease-install-timeout`, 10m by default. The operator sets `RetryOnFailure` on both install and upgrade, so a download that outlasts the window is retried as an upgrade until the import finishes. Nothing uninstalls the DataVolumes in between, and the import keeps running. One image that never imports keeps its whole catalog not Ready until the image imports or its entry is removed. There is no time limit, and I kept it that way on purpose. A group/kind health check cannot tell one image from another, so it cannot be made per image. A per-component timeout would be an API change for one opt-in package. Both charts' `values.yaml` document this. Nothing on `main` lists either package in `dependsOn`, and the images that did import stay usable in the meantime. Tenant worker pools look up each golden directly and never wait on the catalog's status. The price is a helm-controller slot. The operator labels the generated HelmRelease with `cozystack.io/package`, plus `cozystack.io/privileged` when privileged. The flux-shard-operator acts only on HelmReleases that already carry `sharding.fluxcd.io/key`. So both catalogs go to the system helm-controller, which picks up releases without that key and runs with `--concurrent=5`. A DataVolume in `Failed` ends each attempt at once and costs next to nothing. An import that stalls without reaching `Failed` is the expensive case, for example an importer that keeps retrying an unreachable URL. Each attempt waits out the full window, and the next one starts 30s later. So one of those five slots stays busy about 95% of the time for as long as the image is stuck. #4145 describes the same cost on the shard controllers. The status change alone does not protect a vm-disk, because vm-disk clones from a golden without checking it. A golden still importing is fine, since CDI holds the clone until the source is populated. A golden whose import has Failed is never populated, so the clone would wait forever. vm-disk now refuses to render a new disk on a Failed golden and names the image. This matches the check the worker pools already have. A disk that already exists is exempt, so a golden that fails later does not break that disk's upgrades. The check fires only when CDI sets Failed, and at CDI v1.66.1 an import reaches Failed only on a checksum mismatch. The goldens carry no checksum, so a broken URL keeps the importer retrying and the new disk still waits, with only the catalog's not-Ready status pointing at the image. #4564 tracks that gap. A golden that does not exist at all is a separate gap, tracked in #4548. Tests pin the expressions for both catalogs. A helm-unittest case checks them on the rendered PackageSource. A Go test builds the HelmRelease spec from the shipped file and checks that they arrive with the poller wait strategy. The repo has no harness that evaluates flux CEL, so the phase behaviour above is argued from flux's evaluator and not exercised by a test. The vm-disk check has helm-unittest cases for a Failed, a Succeeded, and a still-importing golden, for a render with no cluster, and for an existing disk. Closes #4169 ### Screenshots <!-- REQUIRED for UI changes: attach screenshots or screen recordings demonstrating the visual impact of your changes. PRs with UI changes without screenshots will not be merged. --> 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: ### Release note ```release-note fix(vm-default-images, kubernetes-worker-image): the golden image catalogs now report Ready only after their images have finished importing, and a failed or stalled import keeps the catalog not Ready. A vm-disk created from a golden image whose DataVolume is in phase Failed is now refused with an error naming the image, instead of waiting forever on a clone that cannot complete. ``` |
||
|
|
de570a3806 |
feat(platform): add slim installation variants (#4595)
## What this PR does This adds three minimal platform variants for small installs, such as arm64 labs, where isp-full is too heavy. They are `isp-slim` for Talos, `isp-slim-generic` for k3s, kubeadm or RKE2, and `isp-hosted-slim` for clusters where the host provides CNI and storage. A slim variant installs only the base platform: the engine, API and dashboard, tenants, ingress and Gateway API. The two non-hosted variants also get LINSTOR. objectstorage-controller stays on too, because cozystack-controller watches BucketClaim unconditionally and crash-loops without that CRD. Everything else is opt-in through `bundles.enabledPackages`, including every paas and naas application and its operator, monitoring, backups, etcd, SeaweedFS, metrics-server, VPA, Multus and MetalLB. The iaas bundle is refused, so there are no VMs and no managed Kubernetes. With the stock preset a slim variant emits 20 Packages (15 for hosted), where isp-full emits 77. On the two non-hosted slim variants, networking is Cilium alone. Without VMs nothing needs Kube-OVN, and Cilium L2 announcements take the place of MetalLB. The admin creates a `CiliumLoadBalancerIPPool` and a `CiliumL2AnnouncementPolicy`, or uses `publishing.externalIPs`. Pod ranges come from `node.spec.podCIDR`, which Talos and k3s allocate by default. `networking.encryption` only drives Kube-OVN IPsec, so the slim variants refuse it instead of silently ignoring it. The tenant application needs its own slim variant. The default one waits on the monitoring, etcd and SeaweedFS applications, so on slim cozystack-basics and tenant-root would never become ready. tenant-rd and cozystack-basics still reference the default variant's artifacts by name. Those artifacts exist anyway, because the PackageSource builds them for every variant. The full variants don't change. I rendered isp-full, isp-full-generic and isp-hosted against main and got byte-identical Packages. The only new output there is the slim variant in the tenant-application PackageSource. Things to know before using it: - An opt-in package does not pull in its dependencies. The presets and the docs list the chains. A package enabled without its chain stays `DependenciesNotReady`. - Slim is for new installs. Switching a live isp-full cluster to isp-slim moves the networking Package from `kubeovn-cilium` to `cilium`, the operator removes the Kube-OVN release, and running pods lose networking. The other Packages are kept by `helm.sh/resource-policy: keep`, so the switch doesn't make the cluster smaller either. - No e2e suite runs a slim variant yet. Helm unit tests cover the presets, opt-in, OIDC, the networking values and both refusals, and a dependency check over every emitted Package passes for all three presets. I installed isp-slim from a build of this branch on a fresh three-node Talos 1.13 cluster (amd64). All Packages and HelmReleases went Ready, with no Kube-OVN, MetalLB, monitoring, backup or KubeVirt pods. From outside, the dashboard and the API answered by name with valid certificates. A LoadBalancer Service got an address from a Cilium pool and answered through L2 announcements. Pod-to-pod traffic across nodes, cluster DNS and a replicated LINSTOR volume worked. Opting in postgres-operator and postgres-application gave a Ready Postgres. That run found the BucketClaim crash-loop above, which is fixed here. The base platform used about 215m CPU and 3.2 GiB of memory, not counting the Kubernetes control plane. ### Screenshots Not a UI change. ### Downstream repositories - [ ] No downstream repository is affected by this change - [x] [cozystack/website](https://github.com/cozystack/website) - follow-up: cozystack/website#718 - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [x] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: cozystack/ansible-cozystack#79 - [x] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: cozystack/ccp#23 - [ ] [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(platform): add the isp-slim, isp-slim-generic and isp-hosted-slim variants. They install only the base platform (engine, dashboard, tenants, ingress, gateway, and LINSTOR outside hosted) with Cilium-only networking and no MetalLB; applications, operators, monitoring, backups, etcd and SeaweedFS are opt-in through bundles.enabledPackages, and the iaas bundle is not available. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added the `isp-slim`, `isp-slim-generic`, and `isp-hosted-slim` platform variants, with support for PaaS and NaaS bundles. * Slim variants use Cilium networking and include a reduced set of packages by default. Additional packages and their dependencies can be enabled as needed. * **Limitations** * IaaS and the platform encryption toggle are not supported on slim variants. Cilium-native encryption is not configured. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
d463e00da1 |
feat(platform): add publishing.serviceDomain for the dashboard Services tab
The dashboard has no way to show a connection hostname for a LoadBalancer Service: before 1.4.0 the old UI composed one from the Service name and the cluster's root host, and operators whose DNS publishes LoadBalancer IPs under a dedicated zone still need that. Add the suffix as a platform value and propagate it into the cozystack-values map as service-domain, next to cluster-domain and branding, so the dashboard chart can read it. The value is display-only and opt-in: empty (the default) means the console shows no hostname, because nothing in the platform publishes DNS for a guessed name. Leading and trailing dots are stripped here, the one place every consumer reads from, so an operator's ".svc.example.org." cannot render as "demo..svc.example.org."; the value is coerced to a string first because an unquoted numeric scalar would otherwise abort the whole platform render. The helm-unittest suite pins the empty default, a configured value landing in the Secret, the dot trimming and the numeric case. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
506dc7a4a7 |
feat(platform): add isp-slim, isp-slim-generic and isp-hosted-slim variants
Small installs such as arm64 labs had to take the whole isp-full set: KubeVirt, backups, monitoring, etcd, SeaweedFS and every paas/naas operator. The slim variants keep only the engine, tenants, ingress, gateway, LINSTOR storage and networking, and turn everything else into opt-in packages listed in bundles.enabledPackages. Full variants render the same Packages as before. Networking on isp-slim and isp-slim-generic is Cilium alone: without VMs nothing needs KubeOVN, and Cilium L2 announcements take the place of MetalLB, which stays available as an opt-in package. KubeOVN IPsec is what networking.encryption drives, so the slim variants refuse it rather than ignore it. isp-hosted-slim keeps the noop datapath. tenant-application gains a slim variant whose dependsOn drops the monitoring, etcd and seaweedfs applications; the default variant waits on them, which would leave cozystack-basics and tenant-root never Ready. The iaas bundle stays limited to the isp-full variants. objectstorage-controller stays on everywhere: cozystack-controller watches BucketClaim unconditionally, so without that CRD its manager never syncs its caches and crash-loops. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
b8bc0dfd5f |
fix(platform): make mongodb-application depend on its operator
The mongodb chart renders kind: PerconaServerMongoDB from psmdb.percona.com/v1, but cozystack.mongodb-application depended only on networking and cozystack-engine. So mongodb-rd registers the ApplicationDefinition as soon as the engine is up, and a tenant can create a MongoDB before the operator exists; its HelmRelease then fails with no matches for kind "PerconaServerMongoDB". This is the same defect #3817 fixed for etcd. Every other operator-backed app here already carries the edge. It also closes a test-selection gap: without it cozystack.mongodb-operator reaches no runnable suite, so every change to the operator escalates to a run of all suites instead of selecting mongodb. select-install.sh --validate reports the graph still has no cycle. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
a09eded9ec |
fix(kubernetes-worker-image): wait for goldens to import before Ready
The Talos worker image catalog renders its goldens the same way the VM image collection does: DataVolumes with immediate bind whose import starts at install. With no health check the release went Ready as soon as helm applied them, so a golden that never imported left the catalog green. Gate it on the DataVolume phase with the same expressions. Worker pools read each golden directly and do not wait on the catalog status, so the gate changes what the status reports and delays no pool. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
51ac18776e |
fix(vm-default-images): wait for golden images to import before Ready
Every image is a DataVolume with immediate bind, so its import starts at install, but nothing waited for it: the HelmRelease went Ready as soon as helm applied the DataVolumes. An image that failed or stalled left the component green, and the failure surfaced later on a vm-disk cloning from it, far from the cause. Gate the release on the DataVolume phase. Only Succeeded counts as ready, and a Failed phase, when CDI sets one, ends the wait at once. Every other phase, and a missing status, keeps the wait open until the install window expires. There is no per-component timeout, so a download longer than that window is retried as an upgrade until it finishes, and the retry strategy never uninstalls the volumes. Signed-off-by: Aleksei Sviridkin <f@lex.la> Assisted-by: LLM |
||
|
|
18b83a72b3 |
feat(platform): make the Proxmox substrate installable from a bundle
Until now the two Cluster API packages behind the Proxmox substrate existed but no bundle emitted them, so a Cozystack install shipped neither and the substrate was reachable only by applying Package CRs by hand. They join the iaas bundle as opt-in packages, where the KubeVirt infrastructure provider is unconditional. The asymmetry is the point: capmox drives a hypervisor outside this cluster and needs a Secret holding a Proxmox API token, which capi-operator reads before it installs anything — with no Secret the provider is never deployed at all. Enabling it by default would ask every Cozystack install that has no Proxmox to supply hypervisor credentials. Enabling capmox brings the in-cluster IPAM provider with it — capmox allocates worker addresses from an InClusterIPPool and has no DHCP mode, so on its own it never gets a ProxmoxCluster to Ready. IPAM stays independently enableable, since assigning addresses is a generic Cluster API concern rather than a Proxmox one. Naming capmox in enabledPackages while disabling IPAM stops the render. The cluster would catch that combination on its own: the capmox PackageSource depends on the IPAM Package, so with that Package absent capmox goes Ready=False with DependenciesNotReady and no HelmRelease is ever created. What it would not do is say why. Failing the render names the two conflicting list entries at the moment someone writes them, instead of leaving a condition to decode later. The negated containsDocument asserts in the wiring suite carry `any: true`. Without it a negated containsDocument passes whether the document is in the render or not, so the two properties the suite exists for — off by default, and disabledPackages winning — could not fail. Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua> Assisted-by: LLM |
||
|
|
67db200530 |
feat(cluster-api): add Proxmox infrastructure provider package
Packages ionos-cloud/cluster-api-provider-proxmox (capmox) the same way capi-providers-infraprovider packages CAPK: vendored components gzipped into a ConfigMap, an InfrastructureProvider that fetches them by label. Two details differ from the KubeVirt package on purpose. The fetchConfig label is `infraprovider-provider: proxmox`, not the shared `infraprovider-components: cozy`. A shared label would put both providers in one match. capi-operator would still install the right one — it keys every matched ConfigMap by its name as the version and takes the one equal to spec.version — but it builds that repository in a single pass and returns an error for the whole list as soon as one match is malformed: a name that does not parse as semver, or missing metadata. Sharing the label would let a broken ConfigMap belonging to one provider stop the other from being installed. The bootstrap packages already separate on a `bootstrap-provider` key; this follows that. components.gz is produced with `gzip -n`. Without it the header carries a timestamp, so regenerating from identical input yields a different blob and the committed artifact appears to change when nothing has. `make components` records the version pin, the source URLs and the flag, so refreshing the vendored manifests is reproducible rather than remembered. It also checks both downloads against the SHA-256 sums recorded in the Makefile before packaging them: a release tag is mutable, so the pin is on the bytes, and bumping CAPMOX_VERSION means recording the new sums next to it. Pinned to the v0.7 series: from v0.8 capmox declares the v1beta2 Cluster API contract, while capi-providers-core here is v1.10.1, which serves v1beta1. The package source depends on the in-cluster IPAM provider and deliberately not on cozystack.kubevirt: the KubeVirt provider needs it because its VMs run inside this cluster, whereas a Proxmox hypervisor is external and reached over its API with the credentials named by `credentialsSecretName`. That Secret is a precondition rather than an optional extra, and the chart does not create it — a hypervisor token belongs to the platform's secret management, not to a Helm release. capi-operator reads the Secret before it installs the provider, so while it is missing nothing is deployed at all. values.yaml carries what an operator needs at that point: that PROXMOX_URL is the bare https://<host>:8006 — capmox appends /api2/json itself, and a URL that already carries it sends every call to /api2/json/api2/json/... and gets 501 back — the full user@realm!tokenid form of PROXMOX_TOKEN, and the fact that a rotated token reaches the manager only after the next provider reconcile and a restart of its pod. Both edges are asserted in tests/sources_capi_provider_proxmox_test.yaml; losing the IPAM edge reproduces a failure mode whose symptom points nowhere near its cause. Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua> Assisted-by: LLM |
||
|
|
9bec21461d |
feat(kubernetes): boot tenant worker disks from a shared golden Talos image (CDI clone) (#4171)
## What this PR does Tenant worker boots from talos OS disk image that CDI streams over HTTP from image factory, and it does that once per worker. Same bytes N times, and N chances for a fetch to fail. In closed or rate-limited environment that is wrong shape. This PR adds opt-in catalog and per-pool selector that reads from it. `packages/system/kubernetes-worker-image` imports one golden DataVolume into `cozy-public` per `(schematicID, version)`, named `talos-worker-<schematicID>-<version>`. Pool opts in with `osImage.builtin` on its own `KubernetesNodes`, and its `disk-system` DataVolume then carries `source.pvc` pointing at that golden, so CDI does storage layer CSI clone instead of HTTP import. One import per flavor, shared by every worker of every tenant. Catalog is disabled by default, enable by adding `cozystack.kubernetes-worker-image` to `bundles.enabledPackages`. Its knobs (`imageFactoryURL`, `storageClass`, `storage`, `images`) are set through `spec.components.platform.values.kubernetesWorkerImage` on cozystack-platform Package. That needed optional package helper to learn components argument, which `cozystack.platform.package.optional.default` did not have, so every optional package registered through that wrapper could name variant and nothing else. Thirteen other packages get same path for free. `osImage` has two mutually exclusive forms: - `osImage.builtin` boots pool workers by CDI-cloning golden from catalog. Clone is storage layer CSI copy, so OS image never crosses pod network and factory answers once for whole fleet instead of once per worker. - `osImage.factory` keeps HTTP import and lets pool pin own `imageFactoryURL`, `schematicID` and `version`. This is bring-your-own path for operator running own factory or caching mirror, and it is what pool gets when `osImage` is omitted. In-guest talos installer written by reconcile job follows same resolution, so pool that pins release boots it and upgrades into it instead of being flipped back to cluster default halfway. This is pool half of what #3950 asks for. That issue wants platform level defaults for two channels, worker OS image and in-guest registry mirrors. Here image channel gets per-pool source and shared cache, platform default plumbing is left to the issue. ## Existing workers do not roll With `osImage` omitted rendered `KubevirtMachineTemplate` is byte identical to what chart produced before. KMT is named by hash of its own content, so any drift would re-image every worker VM in fleet on next upgrade. Stored render snapshot is untouched by this PR, that is the evidence and not the claim. Editing `osImage` on existing pool re-images that pool, like changing any other image reference. ## What fails at render Clone that would silently degrade into something slower or wrong is refused instead of rendered. Golden absent, or its import in terminal `Failed` phase. Pool `storageClass` different from golden one, because CDI cant CSI-clone across storage classes and falls back to host assisted copy over pod network, which is exactly the transfer `osImage.builtin` removes. Empty class on either side resolves against cluster default class before comparing, so pool at `storageClass: ""` is checked too. `diskSize` below golden storage, because clone target cannot be smaller than source. Talos release the matrix lists as unsupported, whether pool reached it through `talos.version` or through `osImage.*` override. Matrix passes talos minor it does not list, so what it promises is that known-bad pairing is refused, not that every pairing is known. `schematicID` that is not plain lowercase alphanumeric, or version that is not `vMAJOR.MINOR.PATCH`, both are interpolated into DataVolume name and image URL while `values.schema.json` types them as bare strings. Golden that is only mid-import is not an error. CDI holds worker disk until source populates, and failing there would abort whole pool HelmRelease for length of a download. Same reasoning covers pool that already cloned: absent or `Failed` golden fails render only for pool still waiting for its first disk, otherwise reconcile of healthy pool would abort over source it no longer reads. Golden that declares no StorageClass is read from its PVC, not from whatever default is in force today, because those differ once cluster default moves. Cluster with two default classes resolves same way Kubernetes does, newest wins. Pool that already cloned is identified by annotation naming exactly that pool, not by disk name prefix, because pool `md0-gpu` produces disks carrying `md0` prefix, and it reads its own class from its own bound PVC, so admin moving `storageclass.kubernetes.io/is-default-class` does not fail release of pool whose disks are all on right class. That resolved class is then written onto worker disk. Reading one class and binding another is how pool at empty `storageClass` passed the comparison and still took host assisted copy on its next worker, so guard and disk now come from one value. HTTP path leaves empty class to cluster as before, which is what keeps its render byte identical. Catalog side refuses duplicate `(schematicID, version)` entries, entries whose schematic or version cannot produce name pool can reconstruct, and edit to `storage`, `storageClass` or source URL of entry whose golden already exists. Last one is not patchable in place, so without guard it lands as rejected patch on next upgrade. Goldens carry `helm.sh/resource-policy: keep`, so dropping entry stops managing golden instead of deleting it. Missing golden aborts whole Kubernetes HelmRelease of every tenant pinned to it, not just the disk, and each cloned worker disk annotates golden it came from, so "does anything still clone this" is answerable before manual delete. Three drift guards live in `hack/check-worker-image-catalog-defaults.bats`, because values that have to agree sit in files nobody edits together. One-sided talos bump trips all three. ## Upgrade Three pool values that never produced working image rendered before and hard-fail now, so pool carrying any of them flips its HelmRelease to `InstallFailed` on upgrade and not on next edit: `talos.version` in `vMAJOR.MINOR` form, `schematicID` that is not plain lowercase alphanumeric, and empty `talos.imageFactoryURL`, which used to render schemeless `/image/...` source URL. None of the three could boot worker, so this is failing earlier and not a regression, but it surfaces on pool nobody touched and fix in each case is to correct the value. CR carrying such value is admitted by API with value intact, and later unrelated edit reaches `.spec.values` of HelmRelease while it stays `InstallFailed`, so edit is taken everywhere and applied nowhere. ## Why e2e does not exercise the clone path Clone is storage layer copy, so it needs StorageClass whose volumes are reachable from any node. Merge-gating e2e has none: those lanes run on talos containers, which share runner kernel and cannot load DRBD, so node-local `local` is only class there. Nothing would arrange the co-location such a class requires either, golden in `cozy-public` has no consumer and binds where CDI importer landed, while worker disk binds where its VM is scheduled. So the suites keep importing over HTTP with `osImage` omitted, which is also the render the no-roll guarantee rests on. This PR touches no e2e harness file: `hack/` differs from main only by the two guards this feature owns, one tying catalog default golden to pool chart talos and StorageClass defaults, one on in-guest installer heredoc. Switching suites over does not work on that substrate. Golden asks for `replicated`, lane does not have it, CDI resolves no StorageProfile, and catalog HelmRelease times out on DataVolume stuck in `ImportInProgress`. Passing lane own class would fix the class and leave the co-location. Nightly and tag build run same suites on QEMU with DRBD and `replicated`, which is the substrate clone path was verified on. That is where e2e coverage belongs, as its own change with that lane storage headroom measured rather than assumed. Worth being straight about what this leaves, because it bears on whether the feature earns its place. The e2e argument was never the strong one: golden fetches from image factory exactly once, and #3513 established that node-join stall tracks host `kvm_amd` `vgif` flag rather than registry egress. What clone genuinely removes is the fanout, N workers streaming over pod network becomes N storage layer copies. That is property of storage layer, so it is worth having where DRBD is and worth nothing where it is not. This ships as production capability with the constraint stated on the field, not as CI improvement. ## Testing On this tree: - full `make unit-tests` plus `make test-controllers`: 95 charts, 2520 helm-unittest cases, 1710 bats cases, `go test ./pkg/...` and `./internal/...`, exit 0 - `pre-commit run --all-files`: exit 0, root `make generate` and per-app generate both clean, so no generated-file drift - render snapshot unchanged, which is what pins byte identical render for pool that does not adopt `osImage` - mutation pass over render guards, each term deleted or inverted in turn and suite required to notice it by name. Restoring presence gate on catalog StorageClass comparison reddens exactly one case, inverting its `ne` to `eq` reddens five. Reverting worker disk to omit its resolved class reddens three cases that pin where builtin disk binds, and forcing that class non-empty on every path reddens HTTP-path case and all three render snapshots, with snapshot diff naming the changed KMT hash `E2E Tests` passed on run 35831830568. That run was on the previous head, 4fd8e7ba, and this head differs from it only by one test comment and the commit message. ## Verified on a live cluster Throwaway three-node talos stand (OCI, talos v1.13.6, LINSTOR/ZFS, `replicated` StorageClass) installed from this branch `packages/` tree. This covers what neither `helm template` nor unit suites can answer. It predates the change that writes resolved StorageClass onto worker disk, and that stand ran with explicit `replicated` on both sides, which is the value that change now renders. - The catalog wires up from `bundles.enabledPackages` alone: `PackageSource cozystack.kubernetes-worker-image` reconciled, its HelmRelease installed, and the golden `talos-worker-<schematic>-v1.13.6` imported to `Succeeded` in 108s (6Gi, RWX, `replicated`). - A pool on `osImage.builtin` clones it on the storage layer. Both worker DataVolumes went `CSICloneInProgress` then `Succeeded` in 43s with `Running=False reason=Completed message="Clone Complete"`, carrying `source.pvc` and no `source.http`, and no importer or upload Pod appeared in the tenant namespace. - The clone yields a bootable talos root disk. Both worker VMs reached `Running`/`Ready`, joined the tenant cluster and went `Ready` about 66s after creation, and the tenant cilium, coredns, csi, ingress-nginx and metrics-server addons all installed on them. - Access modes match across the hop: golden `ReadWriteMany` 6Gi to worker `ReadWriteMany` 20Gi on the same `replicated` class, grown by CDI. - The written annotations are correct in practice: each worker disk carries `kubernetes-worker-image.cozystack.io/golden` naming the golden and `.../pool` naming its pool. - helm-controller converges on a builtin pool: forcing a reconcile left the content-hashed KMT name unchanged and the VMI and DataVolume UIDs identical, so a reconcile does not roll the pool. - Cross-namespace clone RBAC is in place as shipped: `create datavolumes --subresource=source -n cozy-public` resolves to `yes` for a tenant ServiceAccount through the `cdi-clone-dv` RoleBinding, and CDI runs with `cloneStrategyOverride: csi-clone`. This PR adds no RBAC of its own. ### Screenshots Not applicable, no UI change. ### Downstream repositories Trigger map points at two repositories. Provider models every pool field by hand, so `osImage` needs schema, model and expand/flatten pair there. Already tracked as cozystack/terraform-provider-cozystack#39, filed when the field was still called `image`. `content/en/docs/next/operations/configuration/platform-package.md` is a hand-written table of `spec.components.platform.values.*` keys and nothing generates it, so `kubernetesWorkerImage` needs a row there. Filed as cozystack/website#707. - [ ] No downstream repository is affected by this change - [x] [cozystack/website](https://github.com/cozystack/website) - follow-up: https://github.com/cozystack/website/issues/707 - [x] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: https://github.com/cozystack/terraform-provider-cozystack/issues/39 - [ ] [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(kubernetes): tenant worker pools can boot their system disk from a shared golden talos image instead of importing it from the Image Factory once per worker. Enable the opt-in `cozystack.kubernetes-worker-image` package via `bundles.enabledPackages` to publish one golden DataVolume per `(schematicID, version)` in `cozy-public`, then set `osImage.builtin` on a `KubernetesNodes` pool to have CDI clone it on the storage layer. A pool that omits `osImage` keeps importing over HTTP and renders exactly as before. The clone path needs the pool and the golden on the same StorageClass, and one whose volumes are reachable from any node, in practice a replicated/DRBD class. ``` |
||
|
|
bc8c54d6e6 |
feat(kubernetes): boot tenant worker disks from a shared golden Talos image
Every tenant worker VM imports the same Talos raw image from the Image Factory over HTTP, once per worker: a 20-worker cluster fetches the identical ~4.15 GiB artifact 20 times, and each fetch is a live dependency on the Factory at exactly the moment a node joins. packages/system/kubernetes-worker-image is a new opt-in catalog that holds one golden DataVolume per (schematicID, version) in cozy-public, named talos-worker-<schematicID>-<version>. A worker pool opts in with osImage.builtin on its own KubernetesNodes resource, and its disk-system DataVolume then carries source.pvc pointing at that golden, so CDI performs a storage-layer CSI clone instead of an HTTP import -- one import per flavor, shared by every worker of every tenant. Confirmed on a LINSTOR-backed cluster: the worker disk is a CSI clone plus a resize, with no host-assisted upload pod. Both halves are off unless asked for. The catalog is registered through the platform's optional-package helper and disabled by default, so a cluster that does not enable it creates no golden and keeps importing over HTTP. The disk source is derived entirely from osImage, which is absent by default, so a pool that does not set it renders byte-identical to the pre-feature chart and adopting the feature never rolls existing workers. Editing osImage on an existing pool re-images that pool, as changing any other image reference does. What the render refuses, rather than degrading into a silent fallback: - A pool whose golden has no catalog entry, or whose golden terminally failed to import. A golden that is merely mid-import is not an error; CDI holds the worker disk until the source populates. - A pool whose StorageClass differs from its golden's. CDI cannot CSI-clone across classes and falls back to a host-assisted copy over the pod network, which is the transfer this feature exists to remove. An empty class on either side means the cluster default and is resolved before the comparison, and a pool that has already cloned reads its class from its own bound PVC rather than from today's default -- the golden's side is frozen the same way, and an admin moving storageclass.kubernetes.io/is-default-class must not fail the release of a pool whose disks are all on the right class. That resolved class is then written onto the worker disk, so the disk binds where the guard checked instead of on whatever the default has become by then -- reading one class and binding another is how a pool at an empty storageClass passed the comparison and still took the host-assisted copy. The HTTP path leaves an empty class to the cluster as before, which is what keeps its render, and the KubevirtMachineTemplate hash named after it, byte-identical. - A diskSize below the golden's storage size, which a clone target cannot be. - A schematicID that is not a plain lowercase alphanumeric identifier, or a version that is not a vMAJOR.MINOR.PATCH release. Both are interpolated into a DataVolume name and an image URL, and values.schema.json types them as bare strings, so a malformed one otherwise reaches the render. The catalog applies the same patterns, since both sides build the same name from the same fields. - An images[] entry edited after its golden exists. A DataVolume spec cannot be patched in place, so the message names the entry and the supported move -- a new (schematicID, version) -- instead of leaving the API server to reject the patch on the next upgrade. Goldens carry helm.sh/resource-policy: keep, so dropping an entry stops managing a golden rather than deleting it: a missing golden aborts the whole Kubernetes HelmRelease of every tenant pinned to it, not just the disk. Each cloned worker disk annotates the golden it came from and the pool that owns it, so "does anything still clone this?" is answerable before a manual delete instead of after. A builtin pool needs a StorageClass whose volumes are reachable from any node, in practice a replicated/DRBD one. The golden in cozy-public has no consumer and binds as soon as it imports, wherever CDI's importer landed, while a worker disk binds where its VM is scheduled, and nothing here arranges for those to be the same node. The platform ships cloneStrategyOverride: csi-clone, which rules out the host-assisted strategy that would cross nodes. What a node-pinned class does in that situation is not measured, so values.yaml states the requirement and not a symptom. ComputePlane pools forward only the per-pool shape fields, and osImage is excluded there by name: the worker OS image, its Talos installer and its Image Factory are operator decisions about the platform's own images. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
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> |
||
|
|
142a14cb69 |
fix(platform): reject an empty realm name instead of coercing it to cozy
realmName resolved through `| default "cozy"`, which fires on both null and "". Unset (null) legitimately means the default, but an explicit "" is a meaningless value and coercing it hid the mistake. Split the two: null falls back to cozy, an empty string reaches the charset guard and is rejected — the same null-vs-"" distinction keycloakInternalUrl already draws a few lines below. Assisted-by: LLM Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
ff637ed9a2 |
fix(platform): validate the realm-name charset and regenerate kubernetes artifacts
The realm-name guard checked only the type and echoed the parsed value back, so its hint told an operator who wrote `on` to set realmName: "true" — the token YAML had already discarded. Drop the echo and add a slug-charset check: the name is spliced verbatim into URL path segments and a shell token, so a "/" or a space must be rejected at the source rather than split a URL or the hook script downstream. The description edit in the previous commit was hand-applied to the generated kubernetes README and never reached the Go types, leaving pre-commit red; regenerate both with cozyvalues-gen. Finish the oidc-tenant System-mode prose that still read cozy as a fixed name. Assisted-by: LLM Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
949096694b |
fix(platform): reject non-string realm names and shell-quote the hook realm
A realmName that YAML resolves to an int or a bool (a bare 123, or a reserved word like on/off) was coerced with toString, silently renaming the realm to something the operator never typed (on -> "true"). Fail the render with a quoting hint at the source instead of surfacing it later as a broken issuer URL. The update-email hook Job interpolated the realm into its shell script unquoted, so a name carrying a space or a shell metacharacter could break or inject into the script; single-quote it. The Grafana and managed-cluster issuer helpers build the URL with printf %s straight off the channel value, so guard them with toString for the same non-string reason. Name the configured realm in the tenant kube-apiserver oidc.mode description and the Grafana identity-mode comment, which still read cozy as if it were fixed. Assisted-by: LLM Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
80d32b3ab7 |
fix(keycloak): complete realm-name coverage and guard non-string names
The enable-update-email hook Job in keycloak-configure still pinned REALM=cozy, so on a custom realmName it waited on a realm the chart never provisions and the cold install timed out — the one seventh consumer left behind, and the only value nobody could set without breaking the install. A bare-integer or YAML-reserved realm name reached the printf %s that builds the derived keycloakInternalUrl as a Go int, rendering a "%!s(...)" format error into the URL; coerce the name to a string once at the source. Document that renaming the realm on a running cluster provisions a fresh empty realm and orphans the accounts in the old one, and note in the OIDC tenant and grafana guides that the realm name is now the configurable default. info/Makefile carried NAME=etcd, so cozyhr drove the wrong release from that package; correct it while touching the file. Assisted-by: LLM Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
44a9a8e735 |
feat(platform): make the Keycloak realm name configurable
The realm name "cozy" was hardcoded in keycloak-configure (which owns the realm, its clients and the cozystack-cluster-admin roles), in the dashboard and linstor-gui gatekeepers, in the info kubeconfig, and in the Grafana and managed-Kubernetes apiserver OIDC issuers. Operators could not rename the realm without patching vendored templates, which `make update` reverts. The apiserver OIDC issuer is already configurable via talm oidcIssuerUrl, so pointing it at a custom realm left every SSO component on "cozy" and broke login with an issuer mismatch. authentication.oidc.realmName (default "cozy") now rides the _cluster channel so every consumer resolves the same realm, and the default keycloakInternalUrl is derived from it so the realm is named in one place. Assisted-by: LLM Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io> |
||
|
|
5ed076cff8 |
feat(kubevirt): expose migration configuration through platform values
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com> |
||
|
|
b083d8e9e6 |
test(platform): pin the opensearch-operator DNS base wiring
Two ends of the same path. The platform suite pins that the shared values Secret carries networking.clusterDomain to the operator under the subchart key, and the package suite pins that a domain set on that key reaches the manager container as DNS_BASE, with the upstream cluster.local default intact when nothing sets it. Every pattern in the platform suite anchors dnsBase to its opensearch-operator and manager parents rather than matching the key anywhere in the payload. An unanchored pattern is satisfied by any sibling block that happens to carry the same domain, which leaves the assertion green after the block it is meant to pin has been deleted. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
afc003c464 |
feat(postgres): database read-replica autoscaling via KEDA (#3954)
## What this PR does Adds automatic horizontal autoscaling of a PostgreSQL database's **read replicas** in response to load, implementing the merged design proposal [Database Horizontal Autoscaler](https://github.com/cozystack/community/tree/main/design-proposals/database-horizontal-autoscaling) (community#44). The mechanism is entirely stock — no bespoke operator, no new CRD: - **KEDA as an optional platform component** (`packages/system/keda`, wired into `core/platform` as a source + system-bundle entry). It claims the `external.metrics.k8s.io` APIService that nothing served before. - **`cozy-lib.keda.scaledObject` helper** (`packages/library/cozy-lib/templates/_keda.tpl`) renders a KEDA `ScaledObject`, fail-closed on missing/invalid arguments. - **`packages/apps/postgres` autoscaling** — an `autoscaling` values block (`enabled`, `minReplicas`, `maxReplicas`, `metric`, `target`, `maxReplicationLagSeconds`, `dryRun`, `transition`) validated by `values.schema.json`; under active autoscaling the chart renders a `ScaledObject` and seeds `spec.instances` to a constant floor (`max(replicas, effectiveMin)`), while the postgres release is forced to client-side (two-way) Helm apply so the seed is a merge no-op on upgrade and the KEDA-managed HPA — the runtime writer via the CNPG `Cluster` scale subresource — is not reverted. - **Single-value metric (§1)** — one PromQL query returns `Σ(read load over standbys) + target`, so a stock HPA computes `desired = ceil((Σ+target)/target) = 1 + ceil(Σ/target) = primary + desiredRead`, with no per-pod emission. - **The four static brakes (§5)** — quorum floor `max(minReplicas, maxSyncReplicas+1, 2)` (quorum wins over `maxReplicas`), scale-down pacing (`Pods/1/600s`), a replication-lag clamp folded into the query (freeze both directions while lag exceeds the threshold and the primary is writing, with `max_over_time` hysteresis), and dry-run (render paused). - **Two-phase enable/disable migration** via a `transition` sub-flag: phase 1 stages `spec.instances` to the live count and brings KEDA up with its HPA paused in both directions; flipping to phase 2 hands the runtime count to KEDA, and because the seed re-renders the same count the flip is a merge no-op with no dip. - **Observability** — a database-autoscaling Grafana dashboard and PrometheusRule alerts. **Live validation (dev cluster, CNPG 1.30.0).** The full path works end-to-end: the CNPG `Cluster` scale subresource serves `status.selector`, KEDA renders the HPA, the metric reaches it (`ScalingActive=ValidMetricFound`, `TARGETS 151/150`, `desiredReplicas=2 = 1 primary + 1 replica` — the §1 arithmetic), and the lag-clamp query returns the base value on live VictoriaMetrics. Two packaging fixes were needed and are included: KEDA must run on the cozystack `cozy.local` cluster domain (the upstream `cluster.local` default makes the metrics gRPC target NXDOMAIN and every HPA hangs on `FailedGetExternalMetric`), and the operator's client-go throttle is raised so a single-replica operator does not lose its leader lease under scale-subresource polling. Implements #3953. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` against the diff. Two repositories are affected; both are follow-ups to open once this lands (and after its dependency #3951 merges and the values surface is final), left unticked here for a maintainer to confirm rather than claimed with a speculative link: - **cozystack/terraform-provider-cozystack** — this adds the nested `autoscaling` block to `packages/apps/postgres/values.schema.json`. The provider hand-models each app's fields (schema + model + expand/flatten), so it needs the new block to expose it in Terraform. - **cozystack/website** — KEDA is a new platform component (`guides/platform-stack`), and the postgres reference page gains the `autoscaling` params. The managed-app reference is regenerated from `README.md` by the release bot; the platform-stack component list is hand-maintained. - [ ] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: to open (new platform component + postgres autoscaling params) - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: to open (new `autoscaling` block in postgres values.schema.json) ### Depends on - #3951 — bump CloudNativePG to 1.30.0. The HPA can only drive the CNPG `Cluster` scale subresource on CNPG ≥ 1.28.4, where it serves `status.selector`; without it the HPA fails with `InvalidSelector`. This PR should not merge before #3951. ### Release note ```release-note feat(postgres): add optional horizontal autoscaling of read replicas via KEDA, configured through the new `autoscaling` values block; KEDA ships as a new optional platform component. Requires CloudNativePG >= 1.28.4 for the Cluster scale subresource selector. Note: every Postgres release now applies client-side (`release.cozystack.io/helm-server-side-apply: "false"`) so the autoscaler's live `/scale` count is not reverted by Helm; a kind-wide side effect is that chart-rendered fields on Postgres releases are no longer re-asserted by server-side-apply drift correction (driftDetection is unset), a documented trade-off with a per-instance apply strategy as follow-up. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added configurable PostgreSQL read-replica autoscaling with KEDA. * Added CPU and read-connection metrics, replica bounds, replication-lag safeguards, dry-run mode, and transition handling. * Added optional KEDA platform support, Grafana autoscaling dashboard, and Prometheus alerts. * **Documentation** * Clarified integer thresholds, prerequisites, configuration, and transition guidance. * **Bug Fixes** * Improved validation and actionable errors for unavailable KEDA or invalid settings. * **Tests** * Expanded coverage for autoscaling, observability, package selection, and validation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
2055341ba9 |
fix(opensearch-operator): raise vm.max_map_count on every node from a DaemonSet (#4152)
## What this PR does OpenSearch will not start below `vm.max_map_count=262144` when the store may use mmap, and nothing in the platform sets it. The Talos profiles declare no sysctls and no documented node-preparation path sets one, so nodes sit at the kernel default of 65530. The app chart asks the operator to cover that with `spec.general.setVMMaxMapCount`, which appends a privileged init container to every OpenSearch pod. Tenant namespaces carry no PodSecurity label of their own, so they take the cluster default, which Talos ships as baseline through the apiserver's `PodSecurityConfiguration`. That default refuses the container and the StatefulSet never produces a pod. On vanilla kubeadm, kind or k3s the default is `privileged` and the same release comes up, which is why this only bites on a Talos install. The value cannot come from the pod at all. The kernel does not namespace `vm.*`, so kubelet rejects it in `securityContext.sysctls`, and refuses to start if it appears in `--allowed-unsafe-sysctls`. That leaves node configuration or a privileged pod, and a privileged pod belongs in a system namespace rather than in every tenant. This adds a DaemonSet to the `opensearch-operator` package that sets the value once per node from a privileged init container, and marks the component privileged so the platform labels `cozy-opensearch-operator` with `enforce=privileged`. The init container writes to `/proc/sys` and nothing else, with no hostPath. The pod restarts with the node and reapplies the value, so there is nothing to persist. It raises the limit to a floor rather than assigning it, which matters more than it looks. `vm.max_map_count` is node-global, and a stock Ubuntu 24.04 node already reads 1048576 from a `sysctl.d` drop-in. An unconditional write would drop that to 262144 for every workload on the node, on every boot, and undo whatever the operator set it for. So the init container reads the current value first and writes only when it is below the threshold. The idle main container runs its sleep under a shell that traps SIGTERM, so a pod exits on its own when the DaemonSet rolls or a node drains, instead of ignoring the signal and being killed after the full grace period. This does not fix OpenSearch on its own. The app chart still requests the operator's init container through `setVMMaxMapCount: true`, so a release stays blocked until the flip to `false` lands with #2682, after this merges. The DaemonSet has to be in place first: flipping it without one would trade the admission failure for a failed bootstrap check. Carrying the sysctl in the node contract instead, across the Talos profiles and the downstream node-prep paths, is tracked in #4144; that is what would let this DaemonSet be retired. Two things change on upgrade, and neither is visible from the diff. One small DaemonSet pod appears per node in `cozy-opensearch-operator`, and that namespace gains `pod-security.kubernetes.io/enforce=privileged`. That label has no finer granularity than the namespace, so the operator Deployment sitting there leaves the cluster default too, not just the DaemonSet. Keeping the manager at baseline would mean a namespace of its own and another component entry, which did not look worth it, but it is a real cost rather than an accident. The operator is in the `paas` bundle's always-on list, so this lands on every node of every paas installation and raises a node-global kernel tunable there, whether or not anyone ever creates an OpenSearchCluster. That is the price of a node-level sysctl having no cheaper carrier, and it is worth an explicit decision rather than being noticed later. The second is `cozypkg add`, which gates privileged components behind a confirmation or `--allow-privileged`. The gate runs over the resolved dependency closure, and `opensearch-application` dependsOn the operator, so installing the app that way now asks one more question. This is not a new break for scripts: `add` already prompts for a variant on every package it resolves and fails on EOF, so it was never usable non-interactively in the first place. Platform-bundle installs go through the reconciler and are unaffected. ### Screenshots Not a UI change. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` against the diff. It adds a template, a test and a values key under `packages/system/opensearch-operator/`, plus one field in `packages/core/platform/sources/opensearch-operator.yaml`. No new app package, no app schema or version enum, no `ApplicationDefinition`, no installer value, no namespace rename, no variant or bundle, no `hack/` path or make target, and no node prerequisite in `hack/e2e-prepare-cluster.bats`. Nothing in the map matches. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note fix(opensearch-operator): set `vm.max_map_count` on every node from a DaemonSet in the operator's namespace, so OpenSearch no longer depends on a privileged init container that PodSecurity baseline refuses in a tenant namespace. Upgrading adds one small DaemonSet pod per node and labels `cozy-opensearch-operator` with `pod-security.kubernetes.io/enforce=privileged`; installing the package with `cozypkg add` now requires confirmation or `--allow-privileged`. Existing OpenSearch releases are unchanged until a follow-up stops the app chart requesting the operator's own init container. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - OpenSearch Operator installation now runs with the required privileged configuration. - OpenSearch deployments automatically ensure nodes meet the required `vm.max_map_count` setting, improving startup reliability. - Existing higher kernel values are preserved. - Added support for configuring the sysctl image, including use with air-gapped registries. - **Tests** - Added coverage to verify privileged installation, node configuration, security settings, scheduling behavior, and image customization. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
a6caad7c57 |
fix(platform): correct what disabledPackages does to ouroboros (#4098)
<!-- 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 `bundles.disabledPackages` does not uninstall anything. Two comments in the platform chart said it does, offered next to `kubectl delete package.cozystack.io cozystack.ouroboros` as if the two were interchangeable. A name in that list only stops `_helpers.tpl` emitting the Package document, which is the same suppression you already get from leaving `publishing.proxyProtocol` false. Every emitted Package carries `helm.sh/resource-policy: keep`, and that annotation is there precisely so disabling a package stops deleting it, so Helm leaves the Package CR alone. The CR stays, the HelmRelease it owns stays, ouroboros keeps running, and the pre-delete hook never fires because nothing uninstalls the release. Deleting the Package CR is what gets there: GC takes the HelmRelease, Flux uninstalls, the hook runs on the way out. There was a second cost. The guard in the system bundle looks up the live Package CR and never reads `disabledPackages`, so an operator who followed the old advice hit the guard anyway with no idea why. New test case pins that. Comments only in the templates, so rendered output is unchanged. Chart suite green at 156 tests. ### Screenshots Not a UI change. <!-- REQUIRED for UI changes: attach screenshots or screen recordings demonstrating the visual impact of your changes. PRs with UI changes without screenshots will not be merged. --> ### Downstream repositories <!-- Cozystack is upstream for repositories that are NOT kept in sync with it automatically, and nothing in CI compares the two sides, so a change here can break them silently. Which change forces what, and which file to touch, is the trigger map in docs/agents/contributing.md under "Downstream Repositories". Walk it before you tick anything. IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM TO FILL IN: 1. Walk the trigger map against the actual diff of this PR, file by file. Do not decide from the PR title or from your memory of what you changed. 2. For every repository you tick, open the follow-up PR there and link it on the same line. A ticked box with no link claims work that does not exist, which is worse than an empty box. Search that repository for an open PR or issue covering it first, and link that instead of filing a duplicate. 3. When the follow-up is out of scope, or needs a decision that is not yours to make, open an issue in that repository instead, link that, and say so in the PR body. Do not push a speculative PR into another repository just to fill a line here. 4. Do not tick "No downstream repository is affected" to make the checklist go away. If you are unsure, leave every box empty and say so in the PR body, so a human decides. --> Walked the trigger map against the diff. One repo is reached, and I left its box empty rather than tick it without a link. `cozystack/website` has the same wrong instruction verbatim in three files: `content/en/docs/next/networking/hairpin-proxy-protocol.md:122`, and the identical line under `v1.4/` and `v1.5/`. It tells the reader that removing the Package CR "(or add it to `bundles.disabledPackages`)" triggers helm uninstall and fires the pre-delete hook. It also contradicts `operations/configuration/components.md:72` on the same site, which says `disabledPackages` will not remove components that are already installed. The site fix is cozystack/website#683. It turned out to be four files rather than three: `origin/main` there also carries a `v1.6` tree, and all four copies were byte-identical. Nothing else is reached: no package added or renamed, no `values.schema.json`, no version enum, no default, no key the Ansible role sets, nothing under `hack/`. - [ ] No downstream repository is affected by this change - [x] [cozystack/website](https://github.com/cozystack/website) - follow-up: https://github.com/cozystack/website/pull/683 - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note <!-- Write a release note: - Explain what has changed internally and for users. - Start with the same `type(scope):` prefix as in the PR title - Follow the guidelines at https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md. --> ```release-note fix(platform): the platform chart no longer says `bundles.disabledPackages` uninstalls a package. A name in that list only stops the Package document being rendered. `helm.sh/resource-policy: keep` leaves the existing Package CR on the cluster and the component keeps running, so deleting the Package CR is what uninstalls it. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Documentation** - Clarified proxy protocol safety guidance, including the effects of disabling the Ouroboros package and the limitations of using `bundles.disabledPackages`. - Documented that disabling package rendering does not remove an existing live package or stop its associated resources. - **Tests** - Added regression coverage confirming that disabling the package does not bypass the proxy protocol safety guard when the live package remains active. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
675163399d |
fix(platform): accept the container gpu-operator variant in iaas (#4101)
## What this PR does `bundles.iaas.gpuOperatorVariant: container` failed the whole platform render. The `cozystack.gpu-operator` PackageSource ships that variant, `values-container.yaml` configures it deliberately, and `hack/check-gpu-operator-variants.bats` covers it, but the `iaas` bundle guard only accepted `default` and `vgpu`. The exclusion made sense when it was written and stopped making sense a few minutes later. The variant and the guard came from parallel branches, neither an ancestor of the other, variant merged first. Before the guard the bundle hardcoded `default`, so `container` was selected by hand-writing a Package CR, and leaving it out of a bundle-side list cost nothing. The guard made the bundle the variant owner and closed that route without noticing it was one. Its comment calls itself typo protection and names `passthrough` as the example typo. Allowing the value is not the whole fix. The same file puts `extraFeatureGates: [HostDevices]` on the KubeVirt CR whenever gpu-operator is enabled, and builds a `permittedHostDevices` table under it. Both are VM-facing. The container variant keeps the host driver bound and pins `vfioManager` off, so no GPU reaches a VM: the gate would advertise something the node cannot serve, and the table would name resources no sandbox device plugin registers there. Both now render for `default` and `vgpu` only, and `container` gets the gpu-operator Package with the kubevirt component untouched. The helm-unittest case that pinned the rejection is gone. Two replace it: the Package renders at the container variant with no component overrides, and the KubeVirt wiring stays absent even with `gpu.permittedHostDevices` and `gpu.mediatedDevicesConfiguration` set. Comments and docs that called the mirroring unconditional on variant, or the container variant unmerged, are fixed in the same pass. No issue for this one, so the history is in the body. ### Screenshots No UI change. ### Downstream repositories Walked the trigger map against the diff. The only file it touches that appears there is `packages/core/platform/values.yaml`, and the website hit is not the hand-written key table (that page has no GPU section): it is the documented workaround this change makes obsolete. `content/en/docs/next/operations/gpu-container-workloads.md` currently tells readers the bundle rejects `container` and routes them to a hand-crafted Package CR, and `virtualization/gpu.md` / `vgpu.md` describe the KubeVirt mirroring as unconditional on variant. Filed as an issue rather than a PR because the docs must not land ahead of this change, and the `v1.6` copies describe released behaviour and stay correct as they are. Nothing else matches: no package added, renamed or removed, no `values.schema.json` or version enum touched, no `ApplicationDefinition` semantics, no installer values key, no `hack/` layout change, no platform variant or bundle recomposed. `gpuOperatorVariant` appears in none of `ccp`, `ansible-cozystack` or `terraform-provider-cozystack`. - [ ] No downstream repository is affected by this change - [x] [cozystack/website](https://github.com/cozystack/website) - follow-up: https://github.com/cozystack/website/issues/685 - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note fix(platform): accept `container` as a value for `bundles.iaas.gpuOperatorVariant`, so the gpu-operator variant the platform already ships can be selected from platform values instead of a hand-crafted Package CR. The KubeVirt `HostDevices` feature gate and `permittedHostDevices` table are now emitted for the `default` and `vgpu` variants only; `container` serves GPUs to pods and gets neither. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added the `container` GPU operator variant for workloads that do not require VM passthrough. * Container workloads no longer receive KubeVirt host-device or mediated-device GPU wiring. * Clarified GPU configuration behavior for passthrough, vGPU, SR-IOV, and container workloads. * **Documentation** * Updated passthrough guidance, configuration references, examples, and KubeVirt wiring instructions for all supported variants. * Added details for manually configured package resources and platform-specific container setup. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
a3ddba441f |
fix(opensearch-operator): set vm.max_map_count from a node DaemonSet
OpenSearch refuses to start when vm.max_map_count is below 262144 and
the store may use mmap. The kernel default is 65530 and nothing in the
platform's node configuration raises it: the Talos profiles declare no
sysctls, and neither does any documented node-preparation path.
The value cannot come from the OpenSearch pod. The kernel does not
namespace vm.*, so kubelet refuses it in a pod's securityContext.sysctls
and in --allowed-unsafe-sysctls alike, both of which admit namespaced
sysctls only. The operator's own answer, spec.general.setVMMaxMapCount,
appends a privileged init container to every OpenSearch pod. Tenant
namespaces carry no PodSecurity label of their own and take the cluster
default, which Talos ships as baseline through the apiserver's
PodSecurityConfiguration; that default rejects the container, so the
StatefulSet is created and never produces a pod.
Set the value once per node instead, from a DaemonSet in the operator's
own namespace. The init container raises the limit to a floor rather
than assigning it: the tunable is node-global, so a node already tuned
higher for another mmap-heavy workload keeps its own value. The pod runs
at system-node-critical: it is a precondition for every OpenSearch pod on
the node, so it must not lose a scheduling race to the workload it
enables, though priority is not start ordering: a pod that starts first
just fails its own bootstrap check and retries. The idle container runs
the sleep under a shell with a TERM handler, because PID 1 of a
namespace receives only the signals it installed a handler for and a
bare sleep would be SIGKILLed after the full grace period on every
rollout and drain. The write
goes to /proc/sys only, with no hostPath and no edit to the node's
on-disk configuration; the pod restarts with the node and reapplies the
value. The component is declared privileged so the platform labels that
namespace enforce=privileged, which is what this DaemonSet needs and
what tenant namespaces deliberately do not get.
The file returns to a path
|
||
|
|
54621bc046 |
feat(migration): tenant self-service VM import from VMware vSphere
Hidora's customers are leaving VMware now, and a tenant has to be able to import their own machines without a platform administrator. The chart approach could not express that: a tenant can neither create the Secret holding vCenter credentials nor name the VDDK image, so both had to come from somewhere they cannot reach. Two CRDs in a new `forklift.cozystack.io` group, split by lifecycle. A VMImportSource is a long-lived connection -- type, endpoint, credentials, readiness -- and a VMImportTask is the one-shot operation that names machines and produces VMDisks and VMInstances. The outputs carry no owner reference back to the task, so deleting a finished import leaves the machines alone. Konveyor Forklift does the transfer underneath; the controller renders its objects, mirrors its verdicts, and hands each finished volume into a VMDisk without copying it. What a live vCenter migration forced out, and no amount of source reading would have: - A guest copied verbatim keeps the drivers it had under VMware and no virtio, so on a virtio disk it does not fail the import -- it succeeds and then does not boot. Imported disks go on SATA. - VDDK connects straight to the ESXi host at the address vCenter advertises, which is routinely unreachable, and on the first cluster collided with the Service CIDR. `spec.hosts` redirects per host. - Firmware must be carried across or a UEFI guest imports as BIOS and never boots (stacked on #3002). - Deleting a task mid-transfer orphaned its DataVolume; a finalizer now removes the engine's volumes and leaves the outputs. The console gains a Migration section: connections are editable, and the VM field is a picker built from a machine list the controller publishes, since the aggregated API cannot reach the inventory and should not hold a source's credentials to try. Two general mechanisms fall out of that -- an option source may take an argument, and one named in a CRD annotation may reference a sibling field. Design: cozystack/community#62. Docs: cozystack/website#673. Signed-off-by: Andrei Kvapil <kvapss@gmail.com> |
||
|
|
6e8566a94c |
fix(platform): accept the container gpu-operator variant in iaas
The cozystack.gpu-operator PackageSource ships three variants: default, vgpu and container. The iaas bundle's variant guard accepted only the first two, so selecting container failed the render on a value the package ships, values-container.yaml configures deliberately, and the variant bats suite already covers. The exclusion was coherent when it was written and stopped being so immediately. The container variant and the guard landed within minutes of each other from parallel branches, neither an ancestor of the other, variant first. Until the guard, the bundle hardcoded the default variant and container was selected by hand-crafting a Package CR, so leaving it out of a bundle-side list cost nothing. The guard then made the bundle the variant owner and closed that route without noticing it was one; its comment describes itself as typo protection and names passthrough as the example typo. The KubeVirt side is not a bare allowlist edit. The container variant serves GPUs to pods through the device plugin and runs no VM-facing GPU stack: sandboxWorkloads and vfioManager are both off and the host driver stays bound. Both KubeVirt-side pieces the bundle wires are VM-facing — the HostDevices feature gate advertises a capability such a node cannot serve, and permittedHostDevices lists resources no sandbox device plugin registers there. Both are now emitted only for default and vgpu, and container yields the gpu-operator Package with the kubevirt component untouched. Comments and docs that described the KubeVirt mirroring as unconditional on variant, or the container variant as unmerged and unavailable, are corrected to match. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
cd6a9b3a8f |
fix(platform): correct what disabledPackages does to ouroboros
Both the publishing.proxyProtocol comment in values.yaml and the disable-hazard guard comment in the system bundle told operators that adding a package name to bundles.disabledPackages triggers the vendored ouroboros chart's pre-delete Corefile cleanup hook, alongside an explicit `kubectl delete package.cozystack.io cozystack.ouroboros`. It does not. A name in disabledPackages only makes the rendering helper in _helpers.tpl skip emitting the Package document, which is the same suppression that leaving proxyProtocol false already performs. Every emitted Package carries helm.sh/resource-policy: keep, so Helm does not prune the Package it has stopped rendering — that annotation was added precisely to stop disabling a package from deleting it. The Package CR survives, the HelmRelease it owns via ownerReference survives with it, ouroboros keeps running and keeps re-applying its rewrite block to kube-system/coredns, and the pre-delete hook never runs because nothing uninstalls the release. No platform values setting reaches that hook; the operator has to delete the Package CR. Anyone who followed the old advice was left with ouroboros still installed and, on the proxyProtocol false transition, still facing the render guard with no idea why. Pins the behaviour with a case asserting the guard still fails while cozystack.ouroboros sits in disabledPackages and the Package CR is live. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> |
||
|
|
6935df8c7f |
chore(kubevirt): update KubeVirt to v1.9.0 (#4015)
## What this PR does KubeVirt operator manifest goes from v1.8.4 to v1.9.0. The 1.9 release turns every Beta feature gate on by default and adds `disabledFeatureGates` to the CR for opting out, so the CR side needed a few decisions. `Template` is disabled. With it on, virt-operator deploys virt-template: two Deployments, two APIServices, a webhook and a ValidatingAdmissionPolicy that nothing here uses. One more aggregated API server is one more way to break discovery. This one is not a knob. `- Template` is written into the CR unconditionally and there is no value that takes it back out, because nothing on the platform uses VirtualMachineTemplate; if something starts to, the static entry becomes a value then. Asking for it through `extraFeatureGates` is refused at render time with an error that says so, rather than producing a CR KubeVirt would reject. `ImageVolume` is the host-dependent one: off below Kubernetes 1.35, left on from 1.35, with a platform value for runtimes that cannot serve image volumes even there (#3190 has the k3s case). Nothing the platform ships uses containerDisk. The value is `kubevirt.disabledFeatureGates` on the platform values, which the iaas bundle forwards into the emitted `cozystack.kubevirt` Package as `components.kubevirt.values.disabledFeatureGates`: Closes #3190. ```yaml kubevirt: disabledFeatureGates: - ImageVolume ``` The chart refuses to render a CR that names one gate in both lists. KubeVirt's `kubevirt-update-validator.kubevirt.io` webhook rejects that combination and runs with `failurePolicy: Fail`, so on a cluster that already has the CR the write is refused and the `kubevirt` HelmRelease stops converging with everything else the CR carries. The create admitter runs no validation at all (its own comment says the update validator is where that happens, and it only gets installed after the CR exists), and `IsEnabled` in virt-config checks `FeatureGates` before `DisabledFeatureGates`, so on a fresh install the same CR is admitted and the gate silently stays on. Render-time is the only place left where an operator gets told. A name repeated inside one list is not an error: upstream compares the two lists against each other, and both fields are `x-kubernetes-list-type: atomic` in the CRD, so that CR is accepted and the guard stays quiet. The gates the chart pins off can arrive a second time from a supported values file, since the chart writes them itself and an operator listing the gates their cluster declines has no reason to know that, so the range skips them and the CR states each opt-out once. The guard walks its own copy of the seven gates the chart enables. That copy exists because the rendered list has to stay a literal list for `make update` to find its `- VMExport` anchor, and a gate quietly dropped from the copy would keep rendering under `featureGates` while leaving the guard. The suite carries one case per gate, so removing any single name turns exactly one of the seven red. `HotplugVolumes` stays in the list even though 1.9 deprecates it. A deprecated gate listed in the CR is still enabled, and while it is enabled virt-controller skips the declarative hotplug path. Dropping it would switch on declarative hotplug, but then virt-api rejects the VMI-level `removevolume` subresource on VM-owned VMIs, and kubevirt-csi uses that as a fallback during teardown. The CSI driver has to move off that subresource first. Upstream names no removal release for it: the `Deprecated` state's own doc comment says "discontinued in the following release", but `MultiArchitecture` and `DisableMDEVConfiguration` have been deprecated since 1.8.0 and are still registered at 1.9.0, so that is a direction rather than a date. What makes that opt-out list matter is a semantics change: on 1.8 `isFeatureGateEnabled` ended in `return false`, so a Beta gate nobody listed was off; on 1.9 `IsEnabled` ends in `return fg.State == Beta`. Sixteen gates are Beta at v1.9.0 and none of them is in this chart's `featureGates`, so all sixteen arrive on with the bump. Two are switched back off here, one is switched off below Kubernetes 1.35, and I went through the rest: - `Template`: off. Reason above. - `ExternalNetResourceInjection`: off. With it on, virt-controller stops calling `multus.NetworkToResource`, so a NAD's `k8s.v1.cni.cncf.io/resourceName` no longer becomes a resource request on the launcher pod. That is correct for clusters running a network-resources-injector; this platform ships none, so a VM on an SR-IOV NAD would schedule without its device. - `ImageVolume`: off below Kubernetes 1.35, left alone from 1.35 on. KubeVirt renders containerDisk images as `VolumeSource.Image` volumes when the gate is on, and it does not check the host: `dropImageVolumes` in the apiserver strips every image volume and the mounts referencing it while Kubernetes' own `ImageVolume` gate is off, which is the default until 1.35 (`kube_features.go` has it `Default: false` at 1.31 and 1.33). The pod is admitted without the disk and virt-launcher fails looking for it. The management cluster is supported from 1.33, so on 1.33 and 1.34 the chart keeps the KubeVirt gate off and the containerDisk sidecar path in use; from 1.35 the gate stands and `extraFeatureGates` can name it. The apiserver version is a proxy for a runtime capability, not the capability itself, so a cluster at 1.35 or newer whose container runtime cannot serve image volumes (the k3s case in #3190) switches the gate off through `kubevirt.disabledFeatureGates`; that is what the value is for. - `Snapshot`: on. `VirtualMachineSnapshot` needed the gate listed on 1.8; snapshot and export were exercised on the test cluster. - `DeclarativeHotplugVolumes`: on and inert. `handleDeclarativeVolumeHotplug` returns early while `HotplugVolumes` is enabled, which this chart still does. - `GPUsWithDRA`, `HostDevicesWithDRA`: on; each only lets a VMI request a DRA-provisioned device, and nothing here creates one. - `WorkloadEncryptionSEV`: on; needs SEV hardware and a VM that asks for it. - `PasstBinding`: on; needs a VM using the passt binding. - `RebootPolicy`: on; adds a field on the VMI domain spec, unset means the old behaviour. - `VmiMemoryOverheadReport`: on; fills `status.memory.memoryOverhead`, reporting only. - `OptOutRoleAggregation`: on; it enables the `RoleAggregationStrategy` field, and aggregation stops only for a CR that sets it to `Manual`. This one does not. - `KubevirtSeccompProfile`: on; installs the profile, and a workload uses it only where `seccompConfiguration` says so. - `NodeRestriction`: on; virt-handler node authorization, needs the Kubernetes `ServiceAccountTokenPodNodeInfo` gate, GA since 1.32. - `LibvirtHooksServerAndClient`: on; pre-migration hooks on the target launcher, and no hooks ship here. - `PodSecondaryInterfaceNamingUpgrade`: flips on; it adds `--upgrade-ordinal-ifaces` to virt-launcher and a pre-migration hook that renames the ordinal secondary interfaces of VMs still carrying them. The test cluster's migrations exercised that path. Five of the seven gates the chart enables (`ExpandDisks`, `LiveMigration`, `AutoResourceLimitsGate`, `GPU`, `VMExport`) reached GA by 1.9 and are inert: `IsEnabled` returns true for a GA gate whether or not the CR names it. I left the list as it is; dropping the five is a follow-up, because it changes what the CR says without changing what the cluster does and it belongs in its own commit. QEMU is 10.1.0 on both sides of this jump; the el9 build revision moves from -10 to -20 and libvirt goes 11.9 to 11.10. A moving build revision is the thing to check rather than a major: [kubevirt/kubevirt#16386](https://github.com/kubevirt/kubevirt/issues/16386), where every existing VM lost live migration, was 1.6.2 to 1.7.0 on qemu-kvm 9.1.0-19 to 9.1.0-20, one build apart within the same QEMU version. So I tested it: VMs running on the 1.8.4 launcher live-migrated onto the 1.9.0 one on a three-node cluster. `make update` in `kubevirt-operator` used to `rm -rf templates` before the fetch and took the local PrometheusRule template with it. Now it only overwrites the file it downloads. `make update` in `kubevirt` now greps for every line of the new block before it passes: the range directive, the `disabledFeatureGates:` key, the three static entries, and the guard header that decides which static entries render. Every insert in that target is guarded by one directive it also inserts, so a hand-merge that keeps the directive and drops a sibling line leaves the guard satisfied and the insert skipped. That shape covers the directive and nothing else. What makes it worth a grep per line here is the static entries: they are the only lines those inserts add that are CR content in their own right rather than a wrapper around a value, so they render on every install and their absence cannot surface as a value that stopped taking effect. The CR just goes back to deploying virt-template and to letting KubeVirt skip NAD resource injection. The target cannot rebuild a partial block, so it names the missing line and stops. The payload lines of the other four blocks are exposed the same way; I checked, and both dropping `- {{ . }}` from the `extraFeatureGates` range and dropping the `permittedHostDevices:` key from its `with` render green and lose the operator's values. That is a wider change than this PR and I left it out. Two sharp edges I know about and left. `kubevirt.disabledFeatureGates: [HostDevices]` on a cluster that also has `cozystack.gpu-operator` in `enabledPackages` makes the bundle forward the same gate into both lists, and the platform renders fine while the child kubevirt HelmRelease then stops on the guard: fail-closed, with the gate named, but diagnosed one level below where it was configured. And the version-conditional opt-out is evaluated at render time, so a cluster upgraded from 1.34 to 1.35 keeps `ImageVolume` disabled until something re-renders the chart; the direction is the safe one, since the gate stays off and containerDisk keeps the path it had on 1.8.4. Two things I decided against doing here. The guard's copy of the gate list is pinned against a name being dropped, not against a name being added to the rendered list only; the copy exists because `make update` needs the literal `- VMExport` anchor, so closing the other direction means changing the self-heal contract. And `hack/run-kubernetes-block-io_test.bats` still names a `virt-launcher:v1.8.4` image in a fixture: inert, the test matches on the container name, and the file is outside this change. Tested on a three-node Talos v1.13.6 / Kubernetes v1.36.2 cluster with Flux suspended. The upgrade took about ten minutes. virt-operator had already deployed virt-template before the CR change landed and removed it afterwards, CRDs included. Two VMs started on the 1.8.4 launcher, one DataVolume-backed and one containerDisk-backed, live-migrated to the 1.9.0 launcher. VMDisk plus VMInstance through the apps API boots. `addvolume` on the VM subresource reaches Ready, `removevolume` on the VMI subresource is accepted. VirtualMachineSnapshot and VirtualMachineExport from it reach Ready. Restore from snapshot and PVC clone failed on that cluster inside the LINSTOR API layer, unrelated to this change. CDI v1.66.0 from #3920 on top of it: HTTP imports to Block and Filesystem, blank population and hotplug all work. `helm unittest` is 26/26 for the kubevirt chart and 142/142 for the platform chart, and the update idempotency script passes its eleven cases. ### Screenshots Not a UI change. ### Downstream repositories Walked the trigger map against the diff. No app added or renamed, no variant, release asset, `ApplicationDefinition` or tooling contract touched, and none of the `packages/core/platform/values.yaml` keys the ansible role sets are in the diff. One repository is reached. `packages/core/platform/values.yaml` gains a top-level key, `kubevirt.disabledFeatureGates`, defaulting to the empty list, and `operations/configuration/platform-package.md` on the website is a hand-written table of `spec.components.platform.values.*` that nothing generates. The row is in cozystack/website#678, which lands after this PR merges. - [ ] No downstream repository is affected by this change - [x] [cozystack/website](https://github.com/cozystack/website) - follow-up: https://github.com/cozystack/website/pull/678 - [ ] [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: ### Release note ```release-note chore(kubevirt): update KubeVirt to v1.9.0. Beta feature gates are on by default upstream now. The platform keeps `Template` off, which would deploy virt-template, and `ExternalNetResourceInjection`, which would hand `resourceName` injection for NetworkAttachmentDefinitions to an injector the platform does not run. `ImageVolume` is disabled on management clusters below Kubernetes 1.35: containerDisk-backed VMs are rendered with native image volumes when it is on, and an apiserver whose own ImageVolume gate is off, the default before 1.35, drops those volumes silently, leaving the VM without its disk. On 1.35 and newer the gate stands, and that is the first release where containerDisk-backed VMs are rendered with image volumes instead of sidecars; the bundled Talos ships Kubernetes 1.36, so this is the default there. A `kubevirt.disabledFeatureGates` list in the platform values opts out of any other gate a cluster cannot carry. ``` |