Aleksei Sviridkin e05c56524e fix(backups): prepare standalone VMDisk restore targets and add VM backup/restore e2e round-trips (#3427)
## What this PR does

Closes the uncovered VM backup path from #3236.
`hack/e2e-chainsaw/backup/` held only a disabled Chainsaw port of the
old `backup.bats`, written against the retired API (`kind:
VirtualMachine`, `BackupJob.storageRef`/`strategyRef`,
`velero-strategy-default`), so it guarded nothing. This removes it,
fixes the in-place restore of a standalone VMDisk in the backup
controller, and adds two real round-trips in the `vminstance` suite that
drive the `examples/backups/{vmi,vmdisk}` scripts, so the documented
demo and the automated test stay in step.

### Controller: standalone VMDisk in-place restore

`collectUnderlyingResources` in
`internal/backupcontroller/velerostrategy_controller.go` returned
nothing for any kind but VMInstance, so `prepareForRestore` did nothing
for a standalone VMDisk: no HelmRelease suspend, no DataVolume delete,
no `keepOriginalPVC` rename. An in-place VMDisk restore then raced Flux
and CDI over the PVC name the data mover was repopulating. A standalone
VMDisk now reports its own disk as its underlying resource, so the
restore preparation already used for VMInstance disks suspends the
`vm-disk-<name>` HelmRelease, deletes its DataVolume and renames the
live PVC to `<name>-orig-<hash>` before Velero recreates it. Unit tests
cover both the VMDisk and the VMInstance branch.

A VMInstance is halted before its disks are touched, but a VMDisk target
is not, and every VMInstance disk is a VMDisk. Renaming the PVC of a
disk that a running VM mounts would leave the original `Terminating`
behind `pvc-protection`; Velero's PVC restore action skips a PVC that
still exists, so the RestoreJob would report `Succeeded` having moved
nothing, and the VM would lose its disk on the next restart. An in-place
VMDisk restore therefore fails before changing anything when a Pod that
has not terminated mounts the disk's PVC. The message names the Pod and
tells the user to stop the VM or restore the VMInstance instead.
`TestPrepareForRestore_VMDiskInUse` covers a running mounting Pod
(refused, PVC not renamed, HelmRelease not suspended), a finished Pod, a
Pod on another disk, and a VMInstance target whose own launcher Pod must
not refuse it.

A VMDisk Backup taken before the disk was recorded as its underlying
resource has none, so the disk is derived from the application name
(`vm-disk-<name>`): such a restore is prepared and checked for a
mounting Pod too. The per-disk `orLabelSelectors` stay a VMInstance
concern: a VMDisk strategy already selects its disk, and Velero rejects
a Backup or Restore carrying both `labelSelector` and
`orLabelSelectors`, so appending them for a VMDisk would break a
strategy written with `labelSelector`.

When an in-place restore fails after preparation, nothing is rolled
back. Resuming the disk's HelmRelease while its PVC is renamed and its
DataVolume deleted would have CDI import a fresh disk under the original
name, which looks healthy and hides that the data sits in the `-orig-`
PVC. The failure message and a Warning event instead name the
HelmReleases preparation suspended and the `-orig-` PVCs holding the
original data, read back from the cluster so that a failure before a
step ran does not name what it would have left, and say that a
HelmRelease Velero already restored is resumed, so the disk has to be
checked; `examples/backups/vmdisk/README.md` describes the recovery.
Tests cover both selector paths, the pre-upgrade Backup, a failed Pod
list (the restore requeues and suspends nothing), and all three failures
after preparation: a `PartiallyFailed` Velero Restore, several Restores
owning the job, and a Restore that cannot be created.

Because preparation takes the target apart before Velero validates the
Restore, the RestoreJob is refused before preparation, with nothing
changed, when the Velero Backup is gone (expired or deleted) or not
`Completed`. This is the same single check #4672 added, now run on every
pass behind the preparation failure path, so a Backup that expires after
preparation started names the leftovers too. A Velero Restore that ends
in `FailedValidation` fails the RestoreJob like `Failed`, with Velero's
validation errors and the leftovers in the message, instead of polling
forever. A HelmRelease suspend or a VM halt that does not happen now
stops preparation (requeue) rather than letting the rename and
DataVolume delete run under a live release or VM, and a VMInstance's
disks are renamed only once no Pod mounts them. A VMDisk held only by a
Pod already being deleted is waited for rather than refused, for up to
30 minutes after the RestoreJob started, so a Pod stuck terminating on a
lost node fails the job instead of keeping it `Running`. Each of these
is pinned by a test that turns red when the guard is removed.

Between renaming a kept disk's PVC and deleting its DataVolume, CDI can
recreate an empty PVC under the original name for the still-present
DataVolume. Velero then skips that PVC as already present ("PVC already
exists. Skip restore this PVC."), the RestoreJob reports `Succeeded`,
and the VM boots a blank disk. The round-trip's new DataDownload check
caught this on a VMInstance restore, where the uid, label and `-orig-`
checks had all passed. Once the DataVolume is gone (the wait #4672
added), preparation now deletes a PVC found under the original name
(read uncached) before the Velero Restore is created;
`TestPrepareForRestore_ClearsPVCRecreatedAfterRename` reproduces the
recreation inside the DataVolume delete.

Preparation spans several reconciles, so the RestoreJob records a
`PreparationStarted` condition before it first changes the target; if
recording it fails, preparation retries without changing anything, so a
later failure cannot report leftovers that were never made. A refusal on
a later pass (the Velero Backup expired, a VM started on the disk) then
names what preparation left behind instead of saying nothing was
changed, and a preparation still waiting 30 minutes after it started
fails the same way instead of polling forever. The vmdisk script starts
its consumer Pod only on a PVC that carries the restore label, so it
cannot pin a PVC CDI recreated in the rename window.

Now that a VMDisk target is prepared, `keepOriginalPVC: false` reaches
it as well. Its DataVolume is deleted with orphan propagation, and the
bound PVC is deleted and waited for before the Restore (the path #4672
added for VMInstance disks). Left in place, Velero would skip the PVC
and the CDI webhook would refuse the DataVolume the resumed HelmRelease
creates, leaving the disk's HelmRelease stuck.
`TestPrepareForRestore_VMDiskWithoutKeepDeletesOriginalPVC` pins it.

### `vminstance-2-backup-roundtrip` (un-gated in CI)

A tiny cirros guest (`u1.nano`, 2Gi, in-place restore only) runs the
current API end to end: Velero strategy, BackupClass, a booted
VMInstance, an ad-hoc `BackupJob` and an in-place `RestoreJob`, each
waited to `Succeeded` and failing fast on `Failed`. The VM must come
back `Running`, and its disk PVC must have been created by the restore:
a new `metadata.uid`, Velero's `velero.io/restore-name` label, and the
`vm-disk-<name>-orig-<hash>` PVC that the rename leaves behind. The
label alone does not prove the data moved, because with
`existingResourcePolicy: update` Velero also stamps it on a PVC it only
patched, and the patch path never reaches the data mover. An in-guest
signal is not available on this platform: the KubeVirt CR disables
serial-console logging cluster-wide and cirros ships no
qemu-guest-agent.

### `vmdisk-2-backup-roundtrip` (un-gated in CI)

The disk-only companion: import a 5Gi Ubuntu disk, back it up, restore
it in place relying on the controller fix above, and require the same
three signals (new uid, restore label, `-orig-` PVC), a `Completed`
DataDownload for the restore that wrote bytes, and a Ready HelmRelease.
On a `WaitForFirstConsumer` StorageClass, such as the `local` class the
CI lane uses, the Velero data mover waits until a scheduled Pod has
selected a node for the restored PVC (`WaitPVCConsumed` in Velero's
generic restore exposer). A VMInstance gets that from its virt-launcher
Pod; a detached disk has none, so its `RestoreJob` stays `Running` until
the 30m data-mover prepare timeout fails it. The script therefore starts
a restricted-profile `pause` Pod on the new PVC once Velero has created
it, standing in for the VM that would attach the disk, and removes it
after the restore. `examples/backups/vmdisk/README.md` documents this
for operators restoring a detached disk.

### Harness details

- The disk `storageClass` is an override (default `replicated`). The
Chainsaw steps pass the lane's `COZY_E2E_STORAGE_CLASS` (`local` on the
DRBD-off PR gate).
- Each harness stands up a throwaway in-cluster BSL
(`cozy-e2e-vmi`/`cozy-e2e-vmdisk`) at `seaweedfs-s3.tenant-root:8333`
with `insecureSkipTLSVerify` and `checksumAlgorithm: ""` (the #3236
fixes), reusing the platform bucket and `cozy-backups-creds` under a
distinct prefix, because the platform `cozy-default` BSL derives the
external S3 ingress endpoint, unroutable in the CI cluster.
- The script waits are bounded so that their worst case stays under the
40m op timeout (38m for the VMDisk step), and a hung wait trips the
script's own named timeout rather than a bare `signal: killed`.
- On failure, `catch` also describes the Velero `Restore`,
`DataDownload` and `DataUpload` objects in `cozy-velero`.
- `skipDelete` plus a `finally` that resumes the suspended HelmReleases,
reclaims the `-orig-` PVC and its LINSTOR volume (the PV is flipped back
to `Delete` first), and drains the Velero backups on the throwaway BSL,
branching on kubectl's exit status, before deleting it. When the drain
is not confirmed, the BSL is kept and the step fails, so the Velero
Backups are not orphaned behind a green step.
- HelmRelease waits use the shared `wait_hr_ready`, which fails fast on
`Stalled`. After the VMDisk restore the script first waits for
`observedGeneration` to reach the restored spec's `generation`, because
the `Ready=True` from before the suspend would otherwise satisfy the
wait at once.

The `vmi` example is CI-drivable: `run-all.sh`/`cleanup.sh` are
non-interactive, the VM is waited to boot before the backup,
`volumeSnapshotLocations` is dropped from the demo strategies
(`snapshotMoveData` routes data through the BSL), and
`BACKUP_STORAGE_LOCATION` defaults to `cozy-default`. `select-e2e.sh`
maps `examples/backups/{vmi,vmdisk}` edits to the `vminstance` suite,
except `07-restore-to-copy.sh`, which the suite skips
(`SKIP_RESTORE_TO_COPY=1`) and so stays docs-only. A
`packages/system/velero` change selects the `vminstance` suite as well,
since these are the only Tests that run a Velero backup and restore.

### Screenshots

N/A — no UI changes.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the diff
(backup controller restore preparation for VMDisk, shell scripts under
`examples/backups/`, Chainsaw tests, `hack/select-e2e.sh` routing, a
deleted stale suite, one doc line): no `packages/apps|extra` add/rename,
no `values.schema.json`/enum/default changes, no
CRD/ApplicationDefinition/label/annotation/namespace/metric changes. The
controller change alters what a VMDisk `RestoreJob` does in the cluster,
not any API surface a downstream repository consumes.
`cozystack/examples` holds Terraform for bare nodes, unrelated to the
in-repo `examples/` tree.

- [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(backups): an in-place RestoreJob for a standalone VMDisk now suspends the disk's HelmRelease, deletes its DataVolume and (with keepOriginalPVC) renames the original PVC before Velero restores it, instead of racing Flux and CDI over the same PVC; without keepOriginalPVC the original PVC is deleted so Velero recreates it from the backup. Such a restore fails before changing anything when a running Pod, such as the launcher of a VM that uses the disk, mounts the disk's PVC: stop the VM or restore the VMInstance instead. If such a restore fails after the disk was prepared, its message and a Warning event name the HelmRelease preparation suspended and the PVC holding the original data. On a WaitForFirstConsumer StorageClass the data mover still starts only once a Pod consumes the restored PVC, for example when the disk is attached to a VMInstance. An in-place restore with keepOriginalPVC no longer reports Succeeded over an empty PVC that CDI recreated under the disk's name while the original was being kept aside. Adds Chainsaw backup/restore round-trips for VMInstance and standalone VMDisk that run a real Velero backup and restore in CI.
```
2026-10-13 23:40:02 +03:00
2025-10-08 09:43:34 +05:00
2024-02-08 12:04:32 +01:00

Cozystack Cozystack

GitHub Release Apache-2.0 License Commit activity OpenSSF Best Practices OpenSSF Scorecard CLOMonitor Support

Cozystack

Cozystack is a free and open-source platform and framework for building clouds.

Cozystack is a CNCF Sandbox project that was originally built and sponsored by Ænix.

With Cozystack, you can transform a bunch of servers into an intelligent system with a simple REST API for spawning Kubernetes clusters, Database-as-a-Service, virtual machines, load balancers, HTTP caching services, and other services with ease.

Use Cozystack to build your own cloud or provide a cost-effective development environment.

Cozystack user interface

Use-Cases

Documentation

The documentation is located on the cozystack.io website.

Read the Getting Started section for a quick start.

If you encounter any difficulties, start with the troubleshooting guide and work your way through the process that we've outlined.

Versioning

Versioning adheres to the Semantic Versioning principles.
A full list of the available releases is available in the GitHub repository's Release section.

Contributions

Contributions are highly appreciated and very welcomed!

In case of bugs, please check if the issue has already been opened by checking the GitHub Issues section. If it isn't, you can open a new one. A detailed report will help us replicate it, assess it, and work on a fix.

You are welcome to take on the fix yourself. Commits are used to generate the changelog, and their author will be referenced in it.

If you have a concrete feature request, open an issue. For a cross-cutting or architectural change, open a design proposal in the community repo. For questions or early ideas, use GitHub Discussions. See CONTRIBUTING.md for the full routing.

Community

You are welcome to join us in #cozystack, on either CNCF Slack (get an invite) or Kubernetes Slack (get an invite), in our Telegram group, and at our community meetings. Add them to your Google Calendar or iCal for convenience.

Project documents

Document What it covers
Governance How decisions are made and who makes them
Contributor Ladder How to grow from contributor to maintainer
Maintainers Who maintains the project
Code of Conduct What we expect of each other
Security Policy Reporting a vulnerability, and which releases get fixes
Adopters Who runs Cozystack in production
Roadmap Where the project is going

License

Cozystack is licensed under Apache 2.0.
The code is provided as-is with no warranties.

Commercial Support

A list of companies providing commercial support for this project can be found on official site.

Languages
Go 46.8%
Shell 37.7%
TypeScript 7.1%
Go Template 5.5%
Makefile 1.5%
Other 1.2%