Repository navigation
fix(vm-disk)!: refuse a golden-image clone CDI would leave unbound - #4796
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to The change adds a render-time refusal for golden-image clones that CDI would leave unbound. It exempts existing disks and renders when lookup data is missing. No actionable merge-blocking risk is evident from the supplied context. Pre-merge checks |
|
bfe411e to
fa78367
Compare
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
NOT LGTM
The refusal mirrors CDI's planner correctly. Two things keep it from merging: it blocks a working cross-class clone with no per-disk way out, and its "existing disk is exempt" assumption holds only while the DataVolume is present.
Findings
- [MAJOR]
packages/apps/vm-disk/templates/dv.yaml:107, refuses a working cross-class clone with no per-disk opt-out
- [MAJOR]
packages/apps/vm-disk/templates/dv.yaml:44, existing-disk exemption keyed only on the DataVolume, not the PVC
- [MINOR]
packages/apps/vm-disk/templates/dv.yaml:94, false pass when the common snapshot class object is missing
- [MINOR]
packages/apps/vm-disk/templates/dv.yaml:104, target volume mode read from an unpinned list element
- [MINOR]
packages/apps/vm-disk/templates/dv.yaml:65, guard mirrors CDI planner internals with nothing tying the two to a version
- [NIT]
packages/apps/vm-disk/templates/dv.yaml:106, provisioner read from the StorageClass, not the source PV
Caveats
- Breaking change is partly justified: the node-local case (a
localclass,allowRemoteVolumeAccess: "false") genuinely hangs today, and a loud render error beats a silent forever-pending PVC. The two MAJORs are about the refusal being wider than that case and the exemption being narrower than the real "existing disk" state. - The "clone actually completes on two remote-accessible classes" half of the first MAJOR is a live-cluster property not exercised here (hermetic review); the StorageClass parameters and the author's own note are the basis. A cluster test cloning
replicatedtoreplicated-asyncwould settle it. - Author-acknowledged follow-ups, not graded: a non-expandable target of the same provisioner at equal size still hangs and is not refused; several default classes compare nothing (CDI picks the newest);
source.diskcross-class clones are not covered.
Recommended follow-ups
- Ship the per-disk opt-out the body names, then the refusal can stay strict.
| {{- $targetMode = dig "volumeMode" "Filesystem" (first $propertySets) | toString }} | ||
| {{- end }} | ||
| {{- if and $advanced (eq $goldenProvisioner (dig "provisioner" "" $targetClass | toString)) (eq $targetMode (dig "spec" "volumeMode" "Filesystem" $goldenPVC | toString)) (dig "allowVolumeExpansion" false $targetClass) }} | ||
| {{- fail (printf "source.image %q: golden image %q lives in StorageClass %q, and a clone into StorageClass %q of the same provisioner gets no host-assisted fallback from CDI and may never bind. Set storageClass to %q, or ask the platform administrator to set cloneStrategyOverride to copy in the kubevirt-cdi package." .Values.source.image.name $desiredSource.pvc.name $goldenSC $targetSC $goldenSC) }} |
There was a problem hiding this comment.
[MAJOR] refuses a working cross-class clone with no per-disk opt-out
The guard fires on any target StorageClass of the same provisioner with the same volume mode and expansion, so a golden in replicated cloned into a second remote-accessible LINSTOR class (replicated-async, replicated-nvme) is refused. It never reads allowRemoteVolumeAccess, the one parameter that separates node-local local (line 95, false, where the clone really hangs) from a remote-accessible class (line 115, true, where it binds):
$ grep -c allowRemoteVolumeAccess packages/apps/vm-disk/templates/dv.yaml
0
$ grep -n allowRemoteVolumeAccess hack/e2e-post-install-prep.sh
95: linstor.csi.linbit.com/allowRemoteVolumeAccess: "false"
115: linstor.csi.linbit.com/allowRemoteVolumeAccess: "true"
replicated-async is a shipped class (changelogs v1.3.1, v1.4.0) and v1.6.0 propagates remote-accessible classes to tenants, so this is a normal layout. The PR body itself says such a pair "needs an explicit opt-out", but none ships, and the only escape the error offers is cluster-wide cloneStrategyOverride: copy, which degrades every clone on the platform including same-class ones. Ship a per-disk acknowledge-and-proceed before refusing, or narrow the guard to the node-local case it is actually about.
There was a problem hiding this comment.
I narrowed the guard rather than adding an opt-out. It now refuses only a LINSTOR target whose remote access isn't "from anywhere", read the way linstor-csi v1.11.2 reads it: the key with or without the linstor.csi.linbit.com/ prefix, the value through strconv.ParseBool, and the layer list when the parameter is absent. The mechanism is in linstor-csi's placement step. The clone lands where the source data is, and the scheduler stops early only if one of those nodes is in the requisite set, which this policy decides. So replicated to replicated-async renders now, and other drivers are no longer refused for this.
Two cases may still be refused without need. One is a class without remote access but with Immediate binding, where the requisite set should cover every node; I haven't checked that against external-provisioner. The other is a clone placed on a node that already holds a replica of the golden. Anyone who hits either can use the golden's class or cloneStrategyOverride: copy.
| class, else the Kubernetes default class. CDI breaks a tie between | ||
| several defaults by creation time; that is not reproduced, so a | ||
| tie compares nothing. */}} | ||
| {{- $goldenSC := dig "spec" "storageClassName" "" ($goldenPVC | default dict) | toString }} |
There was a problem hiding this comment.
[MAJOR] existing-disk exemption keyed only on the DataVolume, not the PVC
The new-disk guard block runs under if not $existingDV, and the only existence check is the disk's DataVolume:
$ grep -n 'existingDV :=' packages/apps/vm-disk/templates/dv.yaml
1:{{- $existingDV := lookup "cdi.kubevirt.io/v1beta1" "DataVolume" .Release.Namespace .Release.Name }}
CDI can garbage-collect a completed DataVolume (dataVolumeTTLSeconds), and a PVC can be restored from backup or the DV deleted by hand, leaving a populated PVC with no DataVolume. In that state the chart treats the disk as new and refuses it on the next reconcile or platform upgrade, with no exit because storageClass is immutable. The sibling kubernetes-nodes chart handles exactly this state (tests/worker_image_golden_pvc_only_test.yaml); vm-disk does not. Gate the exemption on the disk's PVC as well as its DataVolume.
There was a problem hiding this comment.
The exemption now checks the disk's PVC as well as its DataVolume. A PVC left behind by DataVolume GC, or restored on its own, skips the golden checks and the class guard. There are tests for both.
| {{- $commonSnapshotClass = "" }} | ||
| {{- end }} | ||
| {{- if $commonSnapshotClass }} | ||
| {{- $advanced = eq (dig "driver" "" (lookup "snapshot.storage.k8s.io/v1" "VolumeSnapshotClass" "" $commonSnapshotClass | default dict) | toString) $goldenProvisioner }} |
There was a problem hiding this comment.
[MINOR] false pass when the common snapshot class object is missing
Under the snapshot strategy, when both StorageProfiles name the same status.snapshotClass but no VolumeSnapshotClass object of that name exists, lookup ... | default dict yields driver "", $advanced stays false and the disk renders. CDI's GetVolumeSnapshotClass returns NotFound and errors the clone, so it never binds, the exact symptom this PR exists to prevent. Low probability (the status field is copied from an admin-set spec), but it is a silent pass on the failure mode.
There was a problem hiding this comment.
This case is refused now, with its own message. GetVolumeSnapshotClass gets the class by name, and the planner returns NotFound as an error before any fallback. It only fires when both sides share a driver, because without a common driver CDI falls back before the lookup.
| {{- $propertySets := dig "status" "claimPropertySets" list $profile }} | ||
| {{- $targetMode := "" }} | ||
| {{- if $propertySets }} | ||
| {{- $targetMode = dig "volumeMode" "Filesystem" (first $propertySets) | toString }} |
There was a problem hiding this comment.
[MINOR] target volume mode read from an unpinned list element
$targetMode is taken from first $propertySets. A real LINSTOR StorageProfile carries several claimPropertySets (Block first, Filesystem last), so first is load-bearing, yet mutating it to last keeps the whole suite green (32/32 pass). With last the guard compares Filesystem against the golden's Block, the volume-mode conjunct goes false, and the guard silently skips a clone it should refuse. Add a case whose target profile has more than one claim property set so this element choice is pinned.
There was a problem hiding this comment.
Added a case where the target profile lists Block first and Filesystem second. With last it goes red.
| {{- end }} | ||
| {{- end }} | ||
| {{- if and $goldenSC $targetSC (ne $goldenSC $targetSC) }} | ||
| {{- /* Mirror CDI's planner (pkg/controller/clone/planner.go at |
There was a problem hiding this comment.
[MINOR] guard mirrors CDI planner internals with nothing tying the two to a version
The refusal reimplements CDI's clone planner (strategy resolution, snapshot-class compatibility, the csi-clone provisioner/mode/expansion fallback). Verified faithful against pkg/controller/clone/planner.go at the pinned v1.66.1. The exposure is drift: a kubevirt-cdi bump that reorders or adds a fallback silently turns this into over- or under-refusing, and the unit tests encode the same assumptions so none would catch it. Leave a re-check marker on the kubevirt-cdi version bump.
There was a problem hiding this comment.
There's a re-check note next to RELEASE in packages/system/kubevirt-cdi-operator/Makefile, the line a CDI bump edits. The guard now also mirrors how linstor-csi parses its parameters, so the same note sits next to LINSTOR_CSI_VERSION in packages/system/linstor/Makefile.
| {{- if $propertySets }} | ||
| {{- $targetMode = dig "volumeMode" "Filesystem" (first $propertySets) | toString }} | ||
| {{- end }} | ||
| {{- if and $advanced (eq $goldenProvisioner (dig "provisioner" "" $targetClass | toString)) (eq $targetMode (dig "spec" "volumeMode" "Filesystem" $goldenPVC | toString)) (dig "allowVolumeExpansion" false $targetClass) }} |
There was a problem hiding this comment.
[NIT] provisioner read from the StorageClass, not the source PV
$goldenProvisioner comes from the golden StorageClass's provisioner, while CDI's GetCommonDriver reads the bound PV's spec.csi.driver. They agree for a dynamically provisioned LINSTOR golden and diverge only for a statically provisioned golden PV, where this would mis-decide. Reasoned, not reproduced.
There was a problem hiding this comment.
The golden's driver now comes from its bound PV's spec.csi.driver, with the class provisioner as the fallback. That's the same order as GetCommonDriver.
When CDI clones a golden image into a StorageClass other than the one the golden PVC lives in, its csi-clone path, and its snapshot path once it gets a VolumeSnapshotClass of the provisioner, check the two classes for a common driver, a matching volume mode and room to expand, and never compare the class names. A target class of the same provisioner therefore gets no host-assisted fallback and the clone is left to the CSI driver. LINSTOR creates that clone next to the source data, so on a target class that does not allow remote volume access from every node, such as a node-local one, the temporary clone PVC stays unbound with nothing surfaced to the user. Refuse such a disk at render time, mirroring the planner's inputs: the CDI override or the target StorageProfile's strategy, the snapshot class CDI would pick, the golden's driver as its PV records it, the target's provisioner, the volume modes and the target's expansion setting, and refuse only a LINSTOR target whose remote access policy, parsed the way linstor-csi parses it, is not plainly "from anywhere". A snapshot class both StorageProfiles name but that does not exist is refused as well, because CDI then fails to plan the clone instead of falling back. An empty storageClass is resolved the way CDI resolves it for a kubevirt DataVolume. A disk whose DataVolume or PVC already exists is exempt, since CDI can garbage-collect a completed DataVolume. Combinations that fall back to a host-assisted copy, targets reachable from any node, and anything else the lookups cannot find still render. This changes behaviour for a new disk in a LINSTOR class without remote access from every node, and for one whose StorageProfiles name a snapshot class that does not exist: both rendered before and are refused now. The way out is the golden image's class, or the copy clone strategy. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
fa78367 to
d288342
Compare
|
IvanHunters all six points are addressed in d288342, details in the threads. With the refusal limited to LINSTOR targets without remote access from anywhere, I didn't add a per-disk opt-out. |
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM
Re-reviewed at d288342. All six points from my earlier round are addressed, and I checked each against the current tree rather than the replies alone.
The guard now reads allowRemoteVolumeAccess and the layer list the way linstor-csi v1.11.2 does, which I confirmed against pkg/volume/remoteaccess.go and pkg/volume/parameter.go at that tag: the parameter with or without the linstor.csi.linbit.com/ prefix, the value through strconv.ParseBool, a rule list in which a rule naming no fromSame means access from anywhere, and the DRBD/NVMe layer-list default when the parameter is absent. A golden in replicated cloned into replicated-async or replicated-nvme renders now, and a driver other than LINSTOR is no longer refused, so the refusal is scoped to the node-local LINSTOR case it was always about.
The existing-disk exemption now keys on the PVC as well as the DataVolume, so a PVC left by DataVolume GC or restored on its own skips the guard. A missing common VolumeSnapshotClass is refused with its own message instead of passing silently. The target volume mode is pinned by a profile whose claim property sets are ordered Block then Filesystem. The golden's driver is read from its bound PV first. The re-check notes sit next to RELEASE in kubevirt-cdi-operator's Makefile and LINSTOR_CSI_VERSION in linstor's.
helm unittest is green at this head (114/114) and stays green on a tree merged with current main. I mutated the three fixes to make sure the tests earn it: reverting first $propertySets to last, dropping the PVC lookup from the exemption, and dropping the remote-access conjunct each redden the suite, so the new tests hold the behaviour they name. Live check-runs on this head are green, Unit & controller tests and E2E (in-tree) among them.
One non-blocking note below. It doesn't hold up the merge.
| {{- if and (kindIs "map" .) (not (get . "fromSame")) }}{{- $remoteFromAnywhere = true }}{{- end }} | ||
| {{- end }} | ||
| {{- end }} | ||
| {{- if and $advanced $sameProvisioner (eq $goldenProvisioner "linstor.csi.linbit.com") (not $remoteFromAnywhere) (eq $targetMode (dig "spec" "volumeMode" "Filesystem" $goldenPVC | toString)) (dig "allowVolumeExpansion" false $targetClass) }} |
There was a problem hiding this comment.
[MINOR] residual over-refusal in the narrowed guard (non-blocking)
Two node-local LINSTOR cases can still be refused when the clone would in fact bind: a class without remote access but with Immediate binding, where the requisite set may already cover every node; and a clone placed on a node that already holds a replica of the golden. Both have an escape (set storageClass to the golden's class, or cloneStrategyOverride: copy), and a disk whose DataVolume or PVC already exists is exempt, so neither blocks an upgrade. Worth a follow-up to confirm the Immediate-binding case against external-provisioner; until then the refusal is the safe default, since the clone it stops was hanging before this change.
There was a problem hiding this comment.
Agreed, the condition reads neither the binding mode nor replica placement, so both cases are refused. Tracked in #4884, starting with the Immediate case.
|
Successfully created backport PR for |
## What this PR does Backports to `release-1.7` from `main`, picked by the rules of the [backport policy](https://github.com/cozystack/cozystack/blob/main/docs/release/backports.md) in merge order. A merged pull request comes as its own commits, picked with `git cherry-pick -x` so each keeps its author, message and sign-offs, and a merge commit that carries the original's merge message with a `Backported-from:` trailer naming it; a squash-merged one comes as one pick. `backport-batch verify` checks that shape. A pick listed as changed differs from the commit it came from; `git range-diff <merge>^1..<merge>^2 <batch merge>^1..<batch merge>^2` shows how. A conflict or a failing check here is a [backport task](https://github.com/cozystack/cozystack/blob/main/docs/release/backport-tasks/README.md). - [#4756](#4756) `fix(backupstrategy-controller): run backup clients on a package-owned image`, 2 commits, changed in the pick: `48d0cb2958a5` - [#4867](#4867) `fix(e2e): keep chainsaw asserts polling through transient API errors`, 1 commit - [#4888](#4888) `fix(ci): dispatch frozen-line builds with GITHUB_TOKEN`, 1 commit - [#4985](#4985) `fix(etcd): stop counting canceled watch streams as failed gRPC requests`, 1 commit - [#5005](#5005) `fix(tcp-balancer): cap HAProxy fds and threads by the pod's own limits`, 1 commit - [#4917](#4917) `fix(kubernetes): run Proxmox CCM v0.16.1 and pin the Proxmox images by digest`, 2 commits - [#4974](#4974) `fix(kubernetes-nodes): plan autoscaler capacity on the cores the VM actually gets`, 1 commit - [#4971](#4971) `fix(e2e): keep the Proxmox tenant kubeconfig per run and remove it`, 2 commits - [#4970](#4970) `fix(e2e): read the KubernetesNodes pool release in the HelmRelease guard`, 3 commits - [#4972](#4972) `fix(e2e): validate the leak allowlist against every suite layout`, 3 commits - [#5082](#5082) `fix(marketplace): pin a tapped repository to a digest instead of a mutable tag`, 15 commits, changed in the pick: `a31ae9090928`, `b326e0badd62`, `a3d883b4bf5b`, `f8509450f7ca` - [#4004](#4004) `fix(dashboard): link a tenant row to its detail page`, 2 commits - [#5212](#5212) `fix(cilium): pass the CiliumNode list to jq on stdin in the IPAM hook`, 1 commit - [#5213](#5213) `fix(kubernetes): move the OIDC users out of the bootstrap Job's script`, 1 commit - [#5208](#5208) `fix(clickhouse): restore the archive a Backup was taken as`, 3 commits - [#5230](#5230) `fix(backups): re-render the default backup objects when the S3 endpoint changes`, 8 commits - [#5133](#5133) `fix(securitygroups): project a membership-only group to a policy Cilium accepts`, 1 commit - [#5091](#5091) `fix(postgres): render the declared databases in a stable order`, 1 commit - [#5123](#5123) `fix(mariadb-operator): derive the cluster name from the platform domain`, 2 commits - [#5151](#5151) `fix(redis): run one Sentinel when Redis runs a single pod`, 5 commits - [#5124](#5124) `fix(lineage, kubernetes): resolve CAPK node VMs to their cluster`, 2 commits - [#5184](#5184) `fix(api): read pool usage under the quota key the controller counts`, 1 commit - [#5190](#5190) `fix(platform): remove the dead registries value and patch-containerd Secret`, 2 commits - [#5211](#5211) `fix(postgres): fail a backup job as soon as its cnpg Backup fails`, 1 commit - [#5155](#5155) `fix(postgres): refuse system databases and never drop them`, 1 commit - [#5207](#5207) `fix(mariadb): fail backup and restore jobs whose operator Job failed`, 5 commits - [#5024](#5024) `fix(kubernetes, monitoring): send tenant cluster metrics to the tenant's vminsert`, 1 commit ### Maintainer overrides - [#5083](#5083) `fix(workloadmonitor): stop doubling the bucket- prefix in bucket Workload names`: `backport/skip`, left out - [#4973](#4973) `fix(kubernetes-nodes): refuse a partial resources spec on every pool`: `backport/skip`, left out - [#5180](#5180) `fix(tests): keep two unit tests green after the release image restamp`: `backport/skip`, left out ### Skipped - Rule 1, already on `release-1.7`: 19 - Rule 2, the title type is not `fix` or `security`, or the scope is `deps`: 26 - Rule 3, a breaking change: 5 - Rule 4, touches platform migrations, `targetVersion` or a CRD, or makes a sizeable API change: 2 <details><summary>Rule 1, already on `release-1.7`</summary> - [#4815](#4815) `fix(vm-disk): name the importer error in the disk's WorkloadsReady message`, `its 2 commits picked, the last in 4323363` - [#4293](#4293) `fix(fluxcd): keep Helm uninstall working during flux-plunger recovery`, `its 1 commit picked, the last in 0210407` - [#4791](#4791) `fix(kubernetes): keep NFS publish from hanging on a dead server`, `its 1 commit picked, the last in fa88bd1` - [#4830](#4830) `fix(multus): drop the CPU limit and make resources configurable`, `its 2 commits picked, the last in 363c842` - [#4865](#4865) `fix(kafka): pass the container limits to the Kafka JVMs`, `its 3 commits picked, the last in fc38a54` - [#4866](#4866) `fix(linstor): mark volumes restored from a snapshot DRBD-initialized`, `its 1 commit picked, the last in f9550b7` - [#4819](#4819) `fix(seaweedfs): build SeaweedFS in-tree with a bounded S3 read-ahead`, `its 5 commits picked, the last in 7f4c346` - [#3427](#3427) `fix(backups): prepare standalone VMDisk restore targets and add VM backup/restore e2e round-trips`, `its 3 commits picked, the last in 4ac0cd5` - [#4889](#4889) `fix(monitoring): send alerts to every Alertmanager replica`, `its 1 commit picked, the last in 5033d53` - [#4929](#4929) `fix(backups): gate every default Strategy CR on its CRD`, `its 2 commits picked, the last in dee1eef` - [#4914](#4914) `fix(fluxcd): close the flux-plunger review follow-ups for the hook, drain and Warnings`, `its 7 commits picked, the last in a8e4916` - [#4939](#4939) `fix(cluster-api): resync capi-operator every minute`, `its 1 commit picked, the last in cc8d6a4` - [#5027](#5027) `fix(hack): keep the backup stack in the vminstance selective install`, `its 1 commit picked, the last in 8ba8386` - [#4955](#4955) `fix(vm-disk): let CDI adopt the PVC a Velero restore recreates`, `its 2 commits picked, the last in 691d7bc` - [#5071](#5071) `fix(marketplace): do not install a connected repository's components on connect`, `its 2 commits picked, the last in 25f8f11` - [#5086](#5086) `fix(tenant): stop the gateway release waiting on TenantGateway listeners and certificates`, `its 1 commit picked, the last in 18b2249` - [#5087](#5087) `fix(controller): carry healthCheckExprs and waitStrategy to existing releases`, `its 1 commit picked, the last in d2a452e` - [#5088](#5088) `fix(ingress-nginx): drop the admission-webhook policy a rebase brought back`, `its 1 commit picked, the last in cf00b41` - [#5142](#5142) `fix(release): keep release bodies within GitHub's size limit`, `its 1 commit picked, the last in c740515` </details> <details><summary>Rule 2, the title type is not `fix` or `security`, or the scope is `deps`</summary> - [#4845](#4845) `docs(governance): welcome new contributors and link LF sanctions guidance`, `type docs` - [#4881](#4881) `feat(bucket): add a ui flag to skip the s3manager web UI`, `type feat` - [#4899](#4899) `chore: link the FOSSA license scan for CLOMonitor`, `type chore` - [#4898](#4898) `feat(backups): back up Valkey with the Redis strategy`, `type feat` - [#4940](#4940) `docs(release): add changelog for v1.6.5`, `type docs` - [#4861](#4861) `feat(release): sign the release assets and attach a source SBOM`, `type feat` - [#3780](#3780) `docs: move the operator guides out of docs/ to where readers find them`, `type docs` - [#4936](#4936) `docs(release): check changelog claims against the code`, `type docs` - [#4959](#4959) `docs(release): point changelog check B at the Talos installer profile`, `type docs` - [#3777](#3777) `docs(agents): define where a new document belongs`, `type docs` - [#5025](#5025) `test(harbor): pin the resources of the nginx proxy and the portal`, `type test` - [#5039](#5039) `feat(keycloak): load additional CA files from Secret or ConfigMap keys`, `type feat` - [#4860](#4860) `feat(kubernetes-nodes): trust a factory CA in the Proxmox template script`, `type feat` - [#5090](#5090) `ci(e2e): end the Proxmox lane's SSH key with a newline`, `type ci` - [#4969](#4969) `test(kubernetes): pin RetryOnFailure on the Proxmox csi release`, `type test` - [#5000](#5000) `test(kubernetes): name the Proxmox fixture's node pool <cluster>-<pool>`, `type test` - [#5168](#5168) `docs(agents): steer agents away from Helm lookup`, `type docs` - [#4997](#4997) `feat(release): attach an SBOM to every release image`, `type feat` - [#3440](#3440) `ci(api-review-gate): publish the gate as a commit status`, `type ci` - [#5034](#5034) `docs: move the repo-only guides onto the website`, `type docs` - [#5215](#5215) `feat(monitoring-agents): export PVC labels from kube-state-metrics`, `type feat` - [#5217](#5217) `feat(monitoring-agents): keep application identity in container log streams`, `type feat` - [#5171](#5171) `feat(api): expose HelmRelease failure state on Application status`, `type feat` - [#5237](#5237) `feat(operator): let the platform source trust a private registry CA`, `type feat` - [#5106](#5106) `feat(release): make backports automatic with backport-batch`, `type feat` - [#4983](#4983) `feat(api): add the reservation evaluator for tenant quotas`, `type feat` </details> <details><summary>Rule 3, a breaking change</summary> - [#4790](#4790) `fix(platform)!: refuse a networking variant change on a live cluster`, `! in the title` - [#4796](#4796) `fix(vm-disk)!: refuse a golden-image clone CDI would leave unbound`, `! in the title` - [#5085](#5085) `fix(kubernetes, opensearch, foundationdb)!: restrict managed application images`, `! in the title` - [#4818](#4818) `fix(keycloak)!: make the KMS proxy keep its keys across restarts`, `! in the title` - [#5167](#5167) `fix(monitoring-agents)!: isolate merged log payload under log_processed`, `! in the title` </details> <details><summary>Rule 4, touches platform migrations, `targetVersion` or a CRD, or makes a sizeable API change</summary> - [#4941](#4941) `fix(backups): keep the backupstrategy-controller Helm release under the Secret size limit`, `a sizeable API change (breaking-change: [v1alpha1] spec.spec.template: field "metadata" was removed, and 5 more), a CRD` - [#5223](#5223) `fix(backups): report sizeBytes for MariaDB and CNPG backup artifacts`, `a CRD` </details> ```backport-batch {"line":"release-1.7","deferred":[]} ``` ### Screenshots See the original pull requests. ### Downstream repositories - [ ] 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(backupstrategy-controller): the ClickHouse (Altinity) and RabbitMQ backup strategy Pods now run a dedicated `backup-http-client` image (alpine + curl + jq + ca-certificates, non-root, ~7 MB) instead of the 349 MB `platform-migrations` image. Air-gapped mirrors need the new `ghcr.io/cozystack/cozystack/backup-http-client` repository. fix(e2e): a chainsaw assert in the e2e suites keeps polling through a transient API error such as an etcd leader change, instead of failing the suite long before its timeout. fix(ci): use GITHUB_TOKEN to start the initial packages build when freezing a release line fix(etcd): the tenant etcd alert etcdHighNumberOfFailedGRPCRequests now counts only the gRPC codes that the etcd mixin and the platform copy of the alert count, so the Canceled watch streams of a Kubernetes API server that stops no longer fire it on every etcd member. fix(tcp-balancer): HAProxy now takes its fd and thread caps from the replica's memory and CPU limits instead of the node, so a t1.nano balancer is no longer OOMKilled on nodes with many CPUs. maxconn follows the memory limit at about 16 connections per MiB, 2035 on t1.nano, and the pods roll once on upgrade. fix(kubernetes): the Proxmox cloud-controller-manager now runs v0.16.1, and the Proxmox CCM and CSI driver images are pinned by digest. The v0.15.0 tag referenced before was re-pushed upstream to a build that panics on start, which left every new Proxmox worker with the uninitialized taint. fix(marketplace): a tapped repository is pinned to a digest instead of following a mutable tag, so moving the tag upstream no longer changes what runs in the cluster; connect accepts oci://...@sha256:... and an optional keyless cosign verify policy (anchored to an exact identity and preserved across re-connects), and with verify flags the CLI verifies the signature and pins the verified digest at tap time. fix(dashboard): tenant name in the tenant list links to its detail page now (where Delete is), and top level tenants get their Edit button back. fix(cilium): the IPAM migration hook no longer fails with "Argument list too long" on clusters with more than a few nodes, which blocked the cilium upgrade and every package that depends on networking. fix(kubernetes): the OIDC bootstrap Job no longer fails to start for a tenant cluster with about 240 or more oidc.users, the bindings now come from a ConfigMap the Job mounts. fix(clickhouse): a RestoreJob now restores the archive its Backup was taken as, not the newest one. New Backups record the archive name, Backups taken before this release are restored best effort from the newest archive stamped no later than their takenAt, with a warning in the restore pod log. A ClickHouse BackupJob created again under a name whose Backup still exists now fails instead of reporting that Backup as its result. fix(backups): the backup controller now re-renders the Velero BSL, the default backup strategies and the platform bucket's credentials Secret when their S3 endpoint is stale, so after the platform bucket's BucketAccess is re-granted for a new publishing.host, backups move to the new endpoint without forcing releases by hand. fix(securitygroups): a SecurityGroup with attachments and no rules no longer leaves an invalid CiliumNetworkPolicy behind; its backing policy is now accepted by Cilium and still allows or denies nothing. fix(postgres): the init script renders the declared databases in a stable order, so an upgrade that leaves the databases unchanged no longer rewrites the init-script Secret. fix(mariadb-operator): the MariaDB operator now uses the platform's networking.clusterDomain instead of a hard-coded cozy.local, so replication and User/Database/Grant reconciliation work on clusters with a non-default cluster domain. fix(redis): an instance with `replicas: 1` now runs one Sentinel instead of three, without a Sentinel PodDisruptionBudget; on upgrade, the PDB the operator created earlier is deleted. Instances with two replicas or more are unchanged. fix(lineage, kubernetes): the virt-launcher pods of Kubernetes worker nodes are now resolved to their Kubernetes application and marked as managed by Cozystack, so a SecurityGroup attached to a Kubernetes cluster now applies to its worker nodes, not only to its control plane. fix(api): tenant quota admission now counts the parent pool's usage of extended resources and hugepages, so a child tenant can no longer get a share the parent has already used. After upgrading, updates to existing tenants may be rejected if their quota for these resources exceeds the parent's remaining budget. This includes updates that leave the quota unchanged. fix(postgres): a Postgres BackupJob now fails as soon as its CloudNativePG Backup fails, with the recorded error, instead of after 30 minutes, and a BackupJob waiting on a CloudNativePG Backup reports a WaitingForCNPGBackup condition with the Backup's phase. fix(postgres): databases now refuses the PostgreSQL system databases postgres, template0 and template1, and the init job never runs DROP DATABASE on them. A system database a previous version took over is handed back to the superuser on the next upgrade (event trigger removed, ownership and public schema restored, its <db>_admin and <db>_readonly roles dropped). A release that declares one of them must remove the entry before upgrading. fix(mariadb): a MariaDB BackupJob or RestoreJob whose operator Job failed now ends Failed instead of Succeeded, and a failed backup no longer creates a Ready Backup without an archive. Backups created by failed runs before this fix are not removed; restoring from one can fail or select another dump from the shared prefix. fix(kubernetes, monitoring): the vmagent of tenant Kubernetes clusters now writes to the tenant's vminsert and labels series with the owning tenant instead of `cozy-monitoring`. Previously no tenant cluster metric reached the tenant's Monitoring. ```
## What this PR does Backports to `release-1.7` from `main`, picked by the rules of the [backport policy](https://github.com/cozystack/cozystack/blob/main/docs/release/backports.md) in merge order. A merged pull request comes as its own commits, picked with `git cherry-pick -x` so each keeps its author, message and sign-offs, and a merge commit that carries the original's merge message with a `Backported-from:` trailer naming it; a squash-merged one comes as one pick. `backport-batch verify` checks that shape. A pick listed as changed differs from the commit it came from; `git range-diff <merge>^1..<merge>^2 <batch merge>^1..<batch merge>^2` shows how. A conflict or a failing check here is a [backport task](https://github.com/cozystack/cozystack/blob/main/docs/release/backport-tasks/README.md). - [#5284](#5284) `fix(seaweedfs): bump the COSI driver to v0.4.0 for the lost-grant fix`, 1 commit ### Maintainer overrides - [#5083](#5083) `fix(workloadmonitor): stop doubling the bucket- prefix in bucket Workload names`: `backport/skip`, left out - [#4973](#4973) `fix(kubernetes-nodes): refuse a partial resources spec on every pool`: `backport/skip`, left out - [#5180](#5180) `fix(tests): keep two unit tests green after the release image restamp`: `backport/skip`, left out ### Skipped - Rule 1, already on `release-1.7`: 46 - Rule 2, the title type is not `fix` or `security`, or the scope is `deps`: 31 - Rule 3, a breaking change: 5 - Rule 4, touches platform migrations, `targetVersion` or a CRD, or makes a sizeable API change: 2 <details><summary>Rule 1, already on `release-1.7`</summary> - [#4815](#4815) `fix(vm-disk): name the importer error in the disk's WorkloadsReady message`, `its 2 commits picked, the last in 4323363` - [#4756](#4756) `fix(backupstrategy-controller): run backup clients on a package-owned image`, `its 2 commits picked, the last in 4643366` - [#4293](#4293) `fix(fluxcd): keep Helm uninstall working during flux-plunger recovery`, `its 1 commit picked, the last in 0210407` - [#4791](#4791) `fix(kubernetes): keep NFS publish from hanging on a dead server`, `its 1 commit picked, the last in fa88bd1` - [#4830](#4830) `fix(multus): drop the CPU limit and make resources configurable`, `its 2 commits picked, the last in 363c842` - [#4865](#4865) `fix(kafka): pass the container limits to the Kafka JVMs`, `its 3 commits picked, the last in fc38a54` - [#4866](#4866) `fix(linstor): mark volumes restored from a snapshot DRBD-initialized`, `its 1 commit picked, the last in f9550b7` - [#4867](#4867) `fix(e2e): keep chainsaw asserts polling through transient API errors`, `its 1 commit picked, the last in df75adc` - [#4819](#4819) `fix(seaweedfs): build SeaweedFS in-tree with a bounded S3 read-ahead`, `its 5 commits picked, the last in 7f4c346` - [#3427](#3427) `fix(backups): prepare standalone VMDisk restore targets and add VM backup/restore e2e round-trips`, `its 3 commits picked, the last in 4ac0cd5` - [#4889](#4889) `fix(monitoring): send alerts to every Alertmanager replica`, `its 1 commit picked, the last in 5033d53` - [#4929](#4929) `fix(backups): gate every default Strategy CR on its CRD`, `its 2 commits picked, the last in dee1eef` - [#4914](#4914) `fix(fluxcd): close the flux-plunger review follow-ups for the hook, drain and Warnings`, `its 7 commits picked, the last in a8e4916` - [#4888](#4888) `fix(ci): dispatch frozen-line builds with GITHUB_TOKEN`, `its 1 commit picked, the last in bf81e2d` - [#4939](#4939) `fix(cluster-api): resync capi-operator every minute`, `its 1 commit picked, the last in cc8d6a4` - [#5027](#5027) `fix(hack): keep the backup stack in the vminstance selective install`, `its 1 commit picked, the last in 8ba8386` - [#4955](#4955) `fix(vm-disk): let CDI adopt the PVC a Velero restore recreates`, `its 2 commits picked, the last in 691d7bc` - [#4985](#4985) `fix(etcd): stop counting canceled watch streams as failed gRPC requests`, `its 1 commit picked, the last in 38e4089` - [#5005](#5005) `fix(tcp-balancer): cap HAProxy fds and threads by the pod's own limits`, `its 1 commit picked, the last in 7a3d0b0` - [#5071](#5071) `fix(marketplace): do not install a connected repository's components on connect`, `its 2 commits picked, the last in 25f8f11` - [#5086](#5086) `fix(tenant): stop the gateway release waiting on TenantGateway listeners and certificates`, `its 1 commit picked, the last in 18b2249` - [#5087](#5087) `fix(controller): carry healthCheckExprs and waitStrategy to existing releases`, `its 1 commit picked, the last in d2a452e` - [#5088](#5088) `fix(ingress-nginx): drop the admission-webhook policy a rebase brought back`, `its 1 commit picked, the last in cf00b41` - [#5142](#5142) `fix(release): keep release bodies within GitHub's size limit`, `its 1 commit picked, the last in c740515` - [#4917](#4917) `fix(kubernetes): run Proxmox CCM v0.16.1 and pin the Proxmox images by digest`, `its 2 commits picked, the last in 8ff6dca` - [#4974](#4974) `fix(kubernetes-nodes): plan autoscaler capacity on the cores the VM actually gets`, `its 1 commit picked, the last in d68a163` - [#4971](#4971) `fix(e2e): keep the Proxmox tenant kubeconfig per run and remove it`, `its 2 commits picked, the last in 77227c5` - [#4970](#4970) `fix(e2e): read the KubernetesNodes pool release in the HelmRelease guard`, `its 3 commits picked, the last in 6b17a76` - [#4972](#4972) `fix(e2e): validate the leak allowlist against every suite layout`, `its 3 commits picked, the last in 7ec550b` - [#5082](#5082) `fix(marketplace): pin a tapped repository to a digest instead of a mutable tag`, `its 15 commits picked, the last in 9cff86f` - [#4004](#4004) `fix(dashboard): link a tenant row to its detail page`, `its 2 commits picked, the last in bd5e0b5` - [#5212](#5212) `fix(cilium): pass the CiliumNode list to jq on stdin in the IPAM hook`, `its 1 commit picked, the last in b28a2e2` - [#5213](#5213) `fix(kubernetes): move the OIDC users out of the bootstrap Job's script`, `its 1 commit picked, the last in 02dd8e5` - [#5208](#5208) `fix(clickhouse): restore the archive a Backup was taken as`, `its 3 commits picked, the last in 9ded5c5` - [#5230](#5230) `fix(backups): re-render the default backup objects when the S3 endpoint changes`, `its 8 commits picked, the last in 490eab5` - [#5133](#5133) `fix(securitygroups): project a membership-only group to a policy Cilium accepts`, `its 1 commit picked, the last in b7c50a5` - [#5091](#5091) `fix(postgres): render the declared databases in a stable order`, `its 1 commit picked, the last in b658dff` - [#5123](#5123) `fix(mariadb-operator): derive the cluster name from the platform domain`, `its 2 commits picked, the last in 17702dd` - [#5151](#5151) `fix(redis): run one Sentinel when Redis runs a single pod`, `its 5 commits picked, the last in 5524a41` - [#5124](#5124) `fix(lineage, kubernetes): resolve CAPK node VMs to their cluster`, `its 2 commits picked, the last in c908560` - [#5184](#5184) `fix(api): read pool usage under the quota key the controller counts`, `its 1 commit picked, the last in 18c17af` - [#5190](#5190) `fix(platform): remove the dead registries value and patch-containerd Secret`, `its 2 commits picked, the last in 617f889` - [#5211](#5211) `fix(postgres): fail a backup job as soon as its cnpg Backup fails`, `its 1 commit picked, the last in 6645e21` - [#5155](#5155) `fix(postgres): refuse system databases and never drop them`, `its 1 commit picked, the last in 6f1bb80` - [#5207](#5207) `fix(mariadb): fail backup and restore jobs whose operator Job failed`, `its 5 commits picked, the last in ef3de55` - [#5024](#5024) `fix(kubernetes, monitoring): send tenant cluster metrics to the tenant's vminsert`, `its 1 commit picked, the last in 423a75b` </details> <details><summary>Rule 2, the title type is not `fix` or `security`, or the scope is `deps`</summary> - [#4845](#4845) `docs(governance): welcome new contributors and link LF sanctions guidance`, `type docs` - [#4881](#4881) `feat(bucket): add a ui flag to skip the s3manager web UI`, `type feat` - [#4899](#4899) `chore: link the FOSSA license scan for CLOMonitor`, `type chore` - [#4898](#4898) `feat(backups): back up Valkey with the Redis strategy`, `type feat` - [#4940](#4940) `docs(release): add changelog for v1.6.5`, `type docs` - [#4861](#4861) `feat(release): sign the release assets and attach a source SBOM`, `type feat` - [#3780](#3780) `docs: move the operator guides out of docs/ to where readers find them`, `type docs` - [#4936](#4936) `docs(release): check changelog claims against the code`, `type docs` - [#4959](#4959) `docs(release): point changelog check B at the Talos installer profile`, `type docs` - [#3777](#3777) `docs(agents): define where a new document belongs`, `type docs` - [#5025](#5025) `test(harbor): pin the resources of the nginx proxy and the portal`, `type test` - [#5039](#5039) `feat(keycloak): load additional CA files from Secret or ConfigMap keys`, `type feat` - [#4860](#4860) `feat(kubernetes-nodes): trust a factory CA in the Proxmox template script`, `type feat` - [#5090](#5090) `ci(e2e): end the Proxmox lane's SSH key with a newline`, `type ci` - [#4969](#4969) `test(kubernetes): pin RetryOnFailure on the Proxmox csi release`, `type test` - [#5000](#5000) `test(kubernetes): name the Proxmox fixture's node pool <cluster>-<pool>`, `type test` - [#5168](#5168) `docs(agents): steer agents away from Helm lookup`, `type docs` - [#4997](#4997) `feat(release): attach an SBOM to every release image`, `type feat` - [#3440](#3440) `ci(api-review-gate): publish the gate as a commit status`, `type ci` - [#5034](#5034) `docs: move the repo-only guides onto the website`, `type docs` - [#5215](#5215) `feat(monitoring-agents): export PVC labels from kube-state-metrics`, `type feat` - [#5217](#5217) `feat(monitoring-agents): keep application identity in container log streams`, `type feat` - [#5171](#5171) `feat(api): expose HelmRelease failure state on Application status`, `type feat` - [#5237](#5237) `feat(operator): let the platform source trust a private registry CA`, `type feat` - [#5106](#5106) `feat(release): make backports automatic with backport-batch`, `type feat` - [#4983](#4983) `feat(api): add the reservation evaluator for tenant quotas`, `type feat` - [#5264](#5264) `feat(release): keep a line's backport record and reports across batch pull requests`, `type feat` - [#5260](#5260) `ci(e2e): skip final cleanup on disposable PR runners`, `type ci` - [#5046](#5046) `feat(backups): record the RDB snapshot location and size for Redis backups`, `type feat` - [#5214](#5214) `feat(observability-proxy): serve tenant namespace logs through an authorizing proxy`, `type feat` - [#5282](#5282) `test(release): isolate report deduplication from workflow attempts`, `type test` </details> <details><summary>Rule 3, a breaking change</summary> - [#4790](#4790) `fix(platform)!: refuse a networking variant change on a live cluster`, `! in the title` - [#4796](#4796) `fix(vm-disk)!: refuse a golden-image clone CDI would leave unbound`, `! in the title` - [#5085](#5085) `fix(kubernetes, opensearch, foundationdb)!: restrict managed application images`, `! in the title` - [#4818](#4818) `fix(keycloak)!: make the KMS proxy keep its keys across restarts`, `! in the title` - [#5167](#5167) `fix(monitoring-agents)!: isolate merged log payload under log_processed`, `! in the title` </details> <details><summary>Rule 4, touches platform migrations, `targetVersion` or a CRD, or makes a sizeable API change</summary> - [#4941](#4941) `fix(backups): keep the backupstrategy-controller Helm release under the Secret size limit`, `a sizeable API change (breaking-change: [v1alpha1] spec.spec.template: field "metadata" was removed, and 5 more), a CRD` - [#5223](#5223) `fix(backups): report sizeBytes for MariaDB and CNPG backup artifacts`, `a CRD` </details> ```backport-batch {"version":2,"line":"release-1.7","built":{"main":"76a34d3e53a14c39d8df63eec6271c2eaae5ffea","line":"3cd9d4c220b519972677c7ff756ae6c4db07ef4a","newer":"","policy":"76a34d3e53a14c39d8df63eec6271c2eaae5ffea","batch":"e27d6630a0008381f0bc105a87d117dfcc29680f","tool":"76a34d3e53a14c39d8df63eec6271c2eaae5ffea","run":"https://github.com/cozystack/cozystack/actions/runs/43209950053","at":"2026-11-12T11:33:16Z","options":{"noWorkflowFiles":true,"mode":"new"}},"report":"9ca0285a0bb30a09736f6a430878b3ce0d32a2811639861e80daa549f208d7b3","closing":null,"deferred":[],"dropped":[],"stale":[]} ``` ```backport-batch-report {"format":1,"id":"9ca0285a0bb30a09736f6a430878b3ce0d32a2811639861e80daa549f208d7b3","line":"release-1.7","built":{"main":"76a34d3e53a14c39d8df63eec6271c2eaae5ffea","line":"3cd9d4c220b519972677c7ff756ae6c4db07ef4a","newer":"","policy":"76a34d3e53a14c39d8df63eec6271c2eaae5ffea","batch":"e27d6630a0008381f0bc105a87d117dfcc29680f","tool":"76a34d3e53a14c39d8df63eec6271c2eaae5ffea","run":"https://github.com/cozystack/cozystack/actions/runs/43209950053","at":"2026-11-12T11:33:16Z","options":{"noWorkflowFiles":true,"mode":"new"}},"overrides":{"backport/include":[],"backport/skip":[4973,5083,5180]},"primary":{"picked":[5284],"missing":[],"deferred":[],"parked":[],"skipped":{"on-line":[3427,4004,4293,4756,4791,4815,4819,4830,4865,4866,4867,4888,4889,4914,4917,4929,4939,4955,4970,4971,4972,4974,4985,5005,5024,5027,5071,5082,5086,5087,5088,5091,5123,5124,5133,5142,5151,5155,5184,5190,5207,5208,5211,5212,5213,5230],"before-start":[],"type":[3440,3777,3780,4845,4860,4861,4881,4898,4899,4936,4940,4959,4969,4983,4997,5000,5025,5034,5039,5046,5090,5106,5168,5171,5214,5215,5217,5237,5260,5264,5282],"breaking":[4790,4796,4818,5085,5167],"touches":[4941,5223],"newer":[],"skip-label":[4973,5083,5180],"dropped":[]}},"overlays":{"dropped":[],"stale":[],"changed":[]},"diag":{},"other":[]} ``` ### Screenshots See the original pull requests. ### Downstream repositories - [ ] 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(seaweedfs): bucket users granted together could get S3 keys that never work (403 InvalidAccessKeyId). COSI driver is bumped to v0.4.0, which writes each grant to per-identity IAM file. ```
…4960) ## What this PR does A VMDisk created from `source.image` with a `storage` smaller than its golden image never comes up. CDI v1.66.1 checks a clone against its source in `validateClone` (`pkg/controller/datavolume/pvc-clone-controller.go`) before it creates the target claim. When the source PVC is in `Block` mode and requests more than the DataVolume, CDI records `CloneValidationFailed` ("target resources requests storage size is smaller than the source"), creates no claim and leaves the DataVolume `Pending` for good. The webhook admits such a DataVolume, so nothing fails at install. Raising `storage` afterwards does not help either, because the chart renders an existing DataVolume's spec as it is (CDI does not let it change) and the resize hook only patches a claim that exists. This is the default path. Every image in `vm-default-images` is 20Gi on `replicated`, the StorageProfile CDI builds for the LINSTOR driver puts `Block` first, and the chart defaults `storage` to 5Gi. Seen on a v1.6.4 cluster with CDI v1.66.1. A 10Gi disk from a 20Gi golden image sat `Pending` with `CloneValidationFailed` while its release kept timing out, and raising the disk to 25Gi left the DataVolume at 10Gi. The only way out is to delete the disk and create it again at 20Gi or more. The chart now refuses such a disk at render time, before any DataVolume exists, with a message that names both sizes and the minimum to set, for example `storage 5Gi is smaller than golden image "vm-default-images-ubuntu" (20Gi), so CDI refuses to clone it and the disk would stay pending forever. Set storage to 20Gi or more.` It compares what CDI compares, the golden PVC's request against the disk's request in bytes, and first raises the disk's request to the target StorageProfile's `cdi.kubevirt.io/minimumSupportedPvcSize` the way `GetEffectiveStorageResources` does. For an empty `storageClass` the target is the class the check from #4796 already resolves, the default virt class or else the Kubernetes default. Nothing is compared, and the disk renders as before, when the golden PVC cannot be read (under `helm template`, or before CDI created it), when the golden PVC is in `Filesystem` mode, and for any source other than `image`. For a `Filesystem` source CDI sizes a `Block` target from the virtual size of the image inside it and otherwise compares requests inflated by a per-class filesystem overhead, neither of which the chart reproduces. An existing disk is exempt, as for the other golden checks, since its DataVolume spec cannot change anyway. Not covered here. A `source.disk` clone into a smaller disk goes through the same CDI check and still renders. The chart default of 5Gi stays below every default image, so a disk created from one at the default size is now refused with the message above instead of hanging. Whether the default should follow the images is a separate decision. A new helm-unittest suite, `tests/dv_golden_size_test.yaml`, has 15 cases. It pins the refusal at the chart default against a 20Gi `Block` golden with the whole message, the equal and the larger size, the comparison in bytes (`21G` is refused against `20Gi`, `20480Mi` renders), a StorageProfile minimum that covers the golden and one that stays below it, the minimum read from the default class for an empty `storageClass`, and the cases that render as before, a `Filesystem` golden, no cluster, a golden PVC that does not exist yet, a disk without a size, an existing disk, an `http` and an `upload` source. Against the template on main the 3 refusal cases fail and the other 12 pass. Six mutations of the new block each turn at least one case red (any volume mode compared, the minimum dropped, the minimum read from the requested class only, no check that `storage` is set, an equal size refused, sizes compared as strings). `helm unittest` passes 129 of 129 for vm-disk, against 114 on main, with helm-unittest 1.1.2 as CI pins it. Merged with the current heads of #4802 or #4955, which touch the same template, vm-disk passes 137 and 131 cases. `helm template` without a cluster renders an image disk at 5Gi as before. I have not run it on a live cluster. The commit does not cherry-pick onto release-1.6 or release-1.7. Neither carries the check from #4796 it builds on, and release-1.6 has none of the golden checks, so a backport would be an adapted change. ### Screenshots Not a UI change. ### Downstream repositories Template and tests only. No values, schema, README, defaults or `ApplicationDefinition` change. The golden images guide on the website already creates its example disk at the image's 20Gi. - [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-disk): a new disk from a golden image is now refused at render time when its storage is smaller than the image, with a message that names both sizes. Before, CDI refused the clone with CloneValidationFailed, the disk stayed pending forever, and raising its storage afterwards did not help. The default images are 20Gi and the default storage is 5Gi, so a disk from one of them needs storage set to 20Gi or more. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Disk sizes are now checked against the size of Block-mode golden images when cloning. If the requested size is too small, it is increased to the target storage profile’s minimum supported size when possible; otherwise, rendering fails with an error that states the required size. * Size comparisons account for actual byte capacity, so equivalent size units are handled correctly. Filesystem-mode sources and other disk sources are unaffected. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
A VM disk created from a golden image into a StorageClass other than the image's one can hang forever: its temporary clone PVC stays unbound, and the user sees nothing. With the default
csi-clonestrategy, CDI checks the source and target classes for a common provisioner, a matching volume mode and room to expand. It never compares class names. So a target class backed by the same provisioner gets no host-assisted fallback, and the clone is left to the CSI driver. LINSTOR creates it next to the source data, so on a target class that does not allow remote volume access from every node (a node-local class, for example) the PVC may never bind.The vm-disk chart now refuses such a disk at render time and tells the user which StorageClass to set. The check follows CDI's own clone planner. The effective strategy comes from the CDI override, or else from the target StorageProfile. It has to be
csi-clone, orsnapshotwhen CDI gets a VolumeSnapshotClass of the provisioner. Once the snapshot path has such a class it runs the same checks as csi-clone, and LINSTOR ships one. Both classes have to share a driver (the golden's is read from its PV, as CDI does), the volume modes have to match, and the target class has to allow expansion. On top of that the chart refuses only a LINSTOR target whoseallowRemoteVolumeAccess, read the way linstor-csi reads it, is not true; without the parameter that means a layer list with no DRBD or NVMe layer. A remote-accessible LINSTOR class and other drivers render. When the provisioner or the volume mode differs, CDI falls back to a host-assisted copy, which completes, so the disk renders as before. A target class without expansion is left alone too, see the follow-ups.The class check compares nothing when a lookup it needs returns nothing: under
helm template, before the golden PVC exists, or without the CDI resource. An emptystorageClassis compared as the class CDI would pick for a kubevirt DataVolume, which is the default virt class, otherwise the Kubernetes default class. Existing disks are exempt, because their DataVolume spec is immutable. A disk counts as existing when its DataVolume or its PVC exists, since CDI can garbage-collect a completed DataVolume. Undersnapshot, a snapshot class that both StorageProfiles name but that does not exist is refused too, for any driver, because CDI then fails to plan the clone instead of falling back.This is a breaking change: a new disk in a LINSTOR class without remote access from every node rendered before and is refused now, and so is one whose StorageProfiles name a snapshot class that does not exist. The first case can include a clone that would happen to work: on a class with
Immediatebinding, which I have not verified, or when the node the clone is placed on already holds a replica of the golden image. The way out is the golden image's class or thecopyclone strategy.How this covers the three fix directions in the issue:
cloneStrategyOverridewhen it is empty already landed on main in fix(cdi): omit the CDI clone strategy override when unset #4138: the template skips the field for an empty or null value, the values schema accepts the empty string, and the chart tests pin both. This PR does not touch it.csi-cloneas the default on purpose. Same-class clones are the common case, andcsi-cloneis the fast path for them. Changing the default is a separate decision, tracked in Decide whether csi-clone stays the platform default CDI clone strategy #4850.Follow-ups this PR does not cover:
snapshoton a cluster that does not serve the snapshot API, StorageProfiles naming a snapshot class still make CDI fail, and the chart does not look that class up.source.disk) into another class of the same provisioner can hang the same way, and nothing refuses it yet.csi-cloneshould remain the platform default now that it can be turned off: Decide whether csi-clone stays the platform default CDI clone strategy #4850.Closes #2908
Screenshots
Not a UI change.
Downstream repositories
No values, schema or ApplicationDefinition change. Re-check notes for the next CDI and linstor-csi bumps sit next to the version pins in the kubevirt-cdi-operator and linstor Makefiles. The new note in the vm-disk README reaches the website through the regular reference-page regeneration.
Release note
Summary by CodeRabbit