diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 2da0b19ebc73..a3ec59d4b52e 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -11240,6 +11240,34 @@ } } }, + "k8s.io.api.core.v1.HostPathVolumeSource": { + "description": "Represents a host path mapped into a pod. Host path volumes do not support ownership management or SELinux relabeling.", + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "description": "path of the directory on the host. If the path is a symlink, it will follow the link to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", + "type": "string", + "default": "" + }, + "type": { + "description": "type for HostPath Volume Defaults to \"\" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath\n\nPossible enum values:\n - `\"\"` For backwards compatible, leave it empty if unset\n - `\"BlockDevice\"` A block device must exist at the given path\n - `\"CharDevice\"` A character device must exist at the given path\n - `\"Directory\"` A directory must exist at the given path\n - `\"DirectoryOrCreate\"` If nothing exists at the given path, an empty directory will be created there as needed with file mode 0755, having the same group and ownership with Kubelet.\n - `\"File\"` A file must exist at the given path\n - `\"FileOrCreate\"` If nothing exists at the given path, an empty file will be created there as needed with file mode 0644, having the same group and ownership with Kubelet.\n - `\"Socket\"` A UNIX socket must exist at the given path", + "type": "string", + "enum": [ + "", + "BlockDevice", + "CharDevice", + "Directory", + "DirectoryOrCreate", + "File", + "FileOrCreate", + "Socket" + ] + } + } + }, "k8s.io.api.core.v1.LocalObjectReference": { "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", "type": "object", @@ -15250,6 +15278,36 @@ } } }, + "v1.OverlayVolumeSource": { + "description": "OverlayVolumeSource represents a qcow2 overlay volume with a backing source. The overlay provides copy-on-write semantics on top of the backing source.", + "type": "object", + "properties": { + "backingFormat": { + "description": "BackingFormat specifies the format of the backing image (raw or qcow2). Defaults to raw if not specified.", + "type": "string" + }, + "backingHostPath": { + "description": "BackingHostPath represents a pre-existing host file or directory used as the backing source.", + "$ref": "#/definitions/k8s.io.api.core.v1.HostPathVolumeSource" + }, + "backingPVC": { + "description": "BackingPVC is a reference to a PersistentVolumeClaim used as the backing (read-only) source.", + "$ref": "#/definitions/v1.PersistentVolumeClaimVolumeSource" + }, + "persistent": { + "description": "Persistent indicates whether the overlay should be kept across VM restarts. If false, the overlay is deleted when the VM stops. Defaults to false.", + "type": "boolean" + }, + "targetHostPath": { + "description": "TargetHostPath specifies where to place the overlay file on the host. If empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks).", + "$ref": "#/definitions/k8s.io.api.core.v1.HostPathVolumeSource" + }, + "targetPVC": { + "description": "TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored. If specified, the overlay will be persisted to this PVC instead of node-local storage.", + "$ref": "#/definitions/v1.PersistentVolumeClaimVolumeSource" + } + } + }, "v1.PITTimer": { "type": "object", "properties": { @@ -17838,6 +17896,10 @@ "type": "string", "default": "" }, + "overlay": { + "description": "Overlay represents a qcow2 overlay volume with explicit backing and target sources. Provides copy-on-write semantics with more control than ephemeral volumes.", + "$ref": "#/definitions/v1.OverlayVolumeSource" + }, "persistentVolumeClaim": { "description": "PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. Directly attached to the vmi via qemu. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims", "$ref": "#/definitions/v1.PersistentVolumeClaimVolumeSource" diff --git a/pkg/ephemeral-disk/BUILD.bazel b/pkg/ephemeral-disk/BUILD.bazel index a4f867a9ba23..fcce5b714b94 100644 --- a/pkg/ephemeral-disk/BUILD.bazel +++ b/pkg/ephemeral-disk/BUILD.bazel @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["ephemeral-disk.go"], + srcs = [ + "ephemeral-disk.go", + "overlay-disk-handler.go", + ], importpath = "kubevirt.io/kubevirt/pkg/ephemeral-disk", visibility = ["//visibility:public"], deps = [ @@ -18,14 +21,17 @@ go_test( srcs = [ "ephemeral-disk_suite_test.go", "ephemeral-disk_test.go", + "overlay-disk-handler_test.go", ], embed = [":go_default_library"], deps = [ "//pkg/ephemeral-disk-utils:go_default_library", "//pkg/libvmi:go_default_library", "//pkg/virt-launcher/virtwrap/api:go_default_library", + "//staging/src/kubevirt.io/api/core/v1:go_default_library", "//staging/src/kubevirt.io/client-go/testutils:go_default_library", "//vendor/github.com/onsi/ginkgo/v2:go_default_library", "//vendor/github.com/onsi/gomega:go_default_library", + "//vendor/k8s.io/api/core/v1:go_default_library", ], ) diff --git a/pkg/ephemeral-disk/OVERLAY_VOLUMES.md b/pkg/ephemeral-disk/OVERLAY_VOLUMES.md new file mode 100644 index 000000000000..617a5cc733ca --- /dev/null +++ b/pkg/ephemeral-disk/OVERLAY_VOLUMES.md @@ -0,0 +1,473 @@ +# Overlay Volumes + +## Overview + +Overlay volumes provide qcow2 copy-on-write functionality for VirtualMachineInstances, enabling fast VM boot times with shared base images while maintaining VM-specific changes in separate overlay files. + +## Architecture + +The overlay volume implementation consists of: + +1. **API Definition** (`staging/src/kubevirt.io/api/core/v1/schema.go`) + - New `OverlayVolumeSource` type added to `VolumeSource` + +2. **Overlay Disk Handler** (`pkg/ephemeral-disk/overlay-disk-handler.go`) + - Creates qcow2 overlay images with backing file references + - Manages overlay lifecycle (creation, cleanup) + - Supports multiple target locations + +3. **Validation** (`pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go`) + - Validates overlay volume configuration + - Enforces persistent/target consistency rules + +4. **Converter** (`pkg/virt-launcher/virtwrap/converter/converter.go`) + - Converts overlay volumes to libvirt domain XML + - Sets up backing store references + +5. **Manager Integration** (`pkg/virt-launcher/virtwrap/manager.go`) + - Calls overlay handler during VM creation + +## Key Features + +- **Flexible Backing Sources**: PVC or HostPath +- **Flexible Target Locations**: PVC, HostPath, or default ephemeral location +- **Persistent vs Ephemeral**: Control overlay lifecycle with `persistent` flag +- **Format Support**: Both raw and qcow2 backing formats +- **Fast Boot**: qcow2 copy-on-write enables instant VM cloning + +## How It Works + +When you define an overlay volume: + +1. **Backing Source** (read-only base image): + - Mounted as `backing-` in the launcher pod + - Can be a PVC or HostPath + +2. **Overlay Creation**: + - A qcow2 file is created at the target location + - Uses `qemu-img create -f qcow2 -b -F ` + - All writes go to the overlay, backing remains unchanged + +3. **Target Location**: + - **No target specified**: `/var/run/kubevirt-private/overlay-disks//disk.qcow2` + - **TargetPVC**: `/var/run/kubevirt-private/vmi-disks/target-/overlay.qcow2` + - **TargetHostPath**: `/.qcow2` + +4. **Cleanup**: + - **persistent=false**: Overlay deleted when VM stops + - **persistent=true**: Overlay survives VM restarts + +## Validation Rules + +| persistent | target | Result | +|-----------|--------|--------| +| false | none | ✅ Valid - ephemeral overlay at default location | +| true | PVC or HostPath | ✅ Valid - persistent overlay at target | +| true | none | ❌ Invalid - can't persist to ephemeral storage | +| false | PVC or HostPath | ❌ Invalid - target implies persistence | + +Additional rules: +- Exactly ONE backing source required (BackingPVC or BackingHostPath) +- At most ONE target (TargetPVC or TargetHostPath) +- `backingFormat` must be `raw` or `qcow2` (defaults to `raw`) + +## YAML Examples + +### Example 1: Basic Ephemeral Overlay (Non-Persistent) + +```yaml +apiVersion: kubevirt.io/v1 +kind: VirtualMachineInstance +metadata: + name: test-vm +spec: + domain: + devices: + disks: + - name: rootdisk + disk: + bus: virtio + resources: + requests: + memory: 1Gi + volumes: + - name: rootdisk + overlay: + backingPVC: + claimName: ubuntu-base-image # Shared read-only base + backingFormat: raw + persistent: false # Changes lost when VM stops +``` + +**Use Case**: Temporary test VMs, CI/CD environments + +**Overlay Location**: `/var/run/kubevirt-private/overlay-disks/rootdisk/disk.qcow2` + +--- + +### Example 2: Persistent Overlay with PVC Target + +```yaml +apiVersion: kubevirt.io/v1 +kind: VirtualMachineInstance +metadata: + name: dev-vm-1 +spec: + domain: + devices: + disks: + - name: rootdisk + disk: + bus: virtio + resources: + requests: + memory: 2Gi + volumes: + - name: rootdisk + overlay: + backingPVC: + claimName: fedora-base-image # Shared base image + targetPVC: + claimName: dev-vm-1-overlay # VM-specific overlay storage + backingFormat: raw + persistent: true # Overlay survives restarts +``` + +**Use Case**: Development VMs, persistent user environments + +**Required PVCs**: +```yaml +# Base image (shared, created once) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: fedora-base-image +spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: 5Gi +--- +# Overlay storage (per-VM, created for each VM) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: dev-vm-1-overlay +spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: 10Gi +``` + +--- + +### Example 3: HostPath Backing and Target + +```yaml +apiVersion: kubevirt.io/v1 +kind: VirtualMachineInstance +metadata: + name: local-vm +spec: + domain: + devices: + disks: + - name: rootdisk + disk: + bus: virtio + resources: + requests: + memory: 2Gi + volumes: + - name: rootdisk + overlay: + backingHostPath: + path: /var/lib/kubevirt/images/centos-base.qcow2 + targetHostPath: + path: /var/lib/kubevirt/vms/local-vm + backingFormat: qcow2 # Base image is qcow2 + persistent: true +``` + +**Use Case**: Local development, single-node clusters + +**Overlay Location**: `/var/lib/kubevirt/vms/local-vm/rootdisk.qcow2` + +--- + +### Example 4: Multiple Overlays in One VM + +```yaml +apiVersion: kubevirt.io/v1 +kind: VirtualMachineInstance +metadata: + name: multi-disk-vm +spec: + domain: + devices: + disks: + - name: os-disk + disk: + bus: virtio + - name: app-disk + disk: + bus: virtio + - name: scratch-disk + disk: + bus: virtio + resources: + requests: + memory: 4Gi + volumes: + # OS disk - persistent overlay from shared base + - name: os-disk + overlay: + backingPVC: + claimName: ubuntu-22-04-base + targetPVC: + claimName: multi-disk-vm-os + backingFormat: raw + persistent: true + + # App disk - persistent overlay with app data + - name: app-disk + overlay: + backingPVC: + claimName: app-base-v1-2-3 + targetPVC: + claimName: multi-disk-vm-app + backingFormat: raw + persistent: true + + # Scratch disk - ephemeral, reset on restart + - name: scratch-disk + overlay: + backingPVC: + claimName: empty-disk-template + backingFormat: raw + persistent: false +``` + +**Use Case**: Multi-tier applications, separate OS/app/data storage + +--- + +## Common Use Cases + +### 1. Fast VM Cloning + +Clone multiple VMs from a single base image: + +``` +Base Image PVC (5GB, shared) + ├── VM-1 Overlay PVC (10GB) → VM-1 + ├── VM-2 Overlay PVC (10GB) → VM-2 + └── VM-3 Overlay PVC (10GB) → VM-3 +``` + +**Benefits**: +- Fast provisioning (no image copy needed) +- Storage efficiency (base image stored once) +- Independent VM changes + +### 2. Golden Images + +Maintain golden OS images and customize per environment: + +```yaml +# Production VM +overlay: + backingPVC: + claimName: rhel-9-golden-image + targetPVC: + claimName: prod-app-server-1 + persistent: true + +# Development VM (same base, different overlay) +overlay: + backingPVC: + claimName: rhel-9-golden-image # Same base + targetPVC: + claimName: dev-app-server-1 # Different overlay + persistent: true +``` + +### 3. CI/CD Test Environments + +Ephemeral test VMs that start fresh each time: + +```yaml +# Test VM reset on each pipeline run +overlay: + backingPVC: + claimName: test-environment-base + persistent: false # Clean slate every run +``` + +### 4. Tiered Storage + +OS on fast storage, data on slower storage: + +```yaml +volumes: +- name: os + overlay: + backingPVC: + claimName: os-base + targetPVC: + claimName: vm-os-ssd # Fast SSD storage + persistent: true + +- name: data + persistentVolumeClaim: + claimName: vm-data-hdd # Slower HDD for data +``` + +## Implementation Details + +### Files Modified/Added + +- `staging/src/kubevirt.io/api/core/v1/schema.go` - API type definition +- `pkg/ephemeral-disk/overlay-disk-handler.go` - Core overlay logic (NEW) +- `pkg/ephemeral-disk/overlay-disk-handler_test.go` - Unit tests (NEW) +- `pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go` - Validation +- `pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go` - Tests +- `pkg/virt-launcher/virtwrap/converter/converter.go` - Domain conversion +- `pkg/virt-launcher/virtwrap/converter/converter_test.go` - Tests +- `pkg/virt-launcher/virtwrap/manager.go` - Integration +- `pkg/virt-controller/services/rendervolumes.go` - Volume rendering +- `api/openapi-spec/swagger.json` - OpenAPI spec + +### qemu-img Command + +The overlay is created using: + +```bash +qemu-img create \ + -f qcow2 \ + -b \ + -F \ + +``` + +Example: +```bash +qemu-img create \ + -f qcow2 \ + -b /var/run/kubevirt-private/vmi-disks/backing-rootdisk/disk.img \ + -F raw \ + /var/run/kubevirt-private/overlay-disks/rootdisk/disk.qcow2 +``` + +### Libvirt Domain XML + +The overlay volume is converted to libvirt XML with backing store: + +```xml + + + + + + + + + +``` + +## Troubleshooting + +### Overlay Creation Fails + +**Error**: `failed to create overlay for volume X` + +**Check**: +1. Does the backing PVC exist and is it bound? +2. Is the backing file accessible in the pod? +3. Is there enough disk space for the overlay? +4. Does the target directory exist (for HostPath)? + +### VM Won't Boot + +**Error**: VM starts but disk is not accessible + +**Check**: +1. Is `backingFormat` correct? (raw vs qcow2) +2. Does the backing file format match what you specified? +3. Check virt-launcher logs: `kubectl logs virt-launcher-` + +### Persistent Overlay Not Working + +**Error**: Changes are lost after VM restart + +**Check**: +1. Is `persistent: true` set? +2. Is a target (TargetPVC or TargetHostPath) specified? +3. Does the target PVC still exist? +4. Check if the overlay file exists at the target location + +### Validation Errors + +**Error**: `persistent=true requires targetPVC or targetHostPath` + +**Fix**: Add a target when using `persistent: true`: +```yaml +overlay: + backingPVC: + claimName: base-image + targetPVC: + claimName: my-overlay # Add this + persistent: true +``` + +**Error**: `targetPVC specified requires persistent=true` + +**Fix**: Set persistent to true when using a target: +```yaml +overlay: + backingPVC: + claimName: base-image + targetPVC: + claimName: my-overlay + persistent: true # Change from false to true +``` + +## Performance Considerations + +### Storage Backend + +- **Fast local storage** (NVMe, SSD) recommended for overlay PVCs +- Base images can be on slower storage (read-only, less I/O) +- Consider local-path-provisioner for single-node setups + +### Overlay Size + +- Overlay PVC should be sized for expected changes +- Thin provisioning recommended to avoid over-allocation +- Monitor overlay growth in production + +### Backing Format + +- **raw format**: Simpler, but larger files +- **qcow2 format**: Compressed, but double-overlay (qcow2 on qcow2) +- For production: raw backing + qcow2 overlay is most common + +## Future Enhancements + +Potential improvements for the overlay volume feature: + +- [ ] Automatic PVC creation for target overlays +- [ ] Overlay size management and warnings +- [ ] Snapshot support for overlay volumes +- [ ] Migration support for VMs with overlay volumes +- [ ] Metrics for overlay disk usage +- [ ] Support for overlay merging back to base +- [ ] Checksum validation of backing sources + +## References + +- qemu-img documentation: https://qemu.readthedocs.io/en/latest/tools/qemu-img.html +- QCOW2 format spec: https://github.com/qemu/qemu/blob/master/docs/interop/qcow2.txt +- KubeVirt volumes: https://kubevirt.io/user-guide/virtual_machines/disks_and_volumes/ + diff --git a/pkg/ephemeral-disk/OVERLAY_VOLUME_STATES.md b/pkg/ephemeral-disk/OVERLAY_VOLUME_STATES.md new file mode 100644 index 000000000000..0463563b6c6a --- /dev/null +++ b/pkg/ephemeral-disk/OVERLAY_VOLUME_STATES.md @@ -0,0 +1,517 @@ +# Overlay Volume Configuration States + +## Complete State Matrix + +This document summarizes all valid and invalid combinations of the `persistent` flag and target location settings for overlay volumes. + +## Field Definitions + +- **`persistent`**: Boolean flag controlling overlay lifecycle + - `true` = overlay survives VM restarts + - `false` = overlay deleted when VM stops + +- **Target Location**: Where the overlay qcow2 file is stored + - `targetPVC` = PersistentVolumeClaim (must be pre-created) + - `targetHostPath` = Directory path on the host node + - `none` = Default ephemeral location + +- **Backing Source**: Where the read-only base image comes from + - `backingPVC` = PersistentVolumeClaim with base image + - `backingHostPath` = File path on the host node + +--- + +## Valid Configurations + +### Configuration 1: Non-Persistent, Default Location + +```yaml +overlay: + backingPVC: + claimName: base-image + backingFormat: raw + persistent: false + # No target specified +``` + +| Field | Value | Result | +|-------|-------|--------| +| persistent | `false` | ✅ | +| targetPVC | `null` | ✅ | +| targetHostPath | `null` | ✅ | + +**Overlay Location**: `/var/run/kubevirt-private/overlay-disks//disk.qcow2` + +**Lifecycle**: +- ✅ Created when VM starts +- ✅ Deleted when VM stops +- ❌ Changes NOT preserved across restarts + +**Use Cases**: +- Ephemeral test VMs +- CI/CD environments +- Temporary workspaces +- VMs that should start fresh each time + +**Storage**: EmptyDir volume (pod-local, deleted with pod) + +--- + +### Configuration 2: Persistent with TargetPVC + +```yaml +overlay: + backingPVC: + claimName: base-image + targetPVC: + claimName: vm-overlay-storage + backingFormat: raw + persistent: true +``` + +| Field | Value | Result | +|-------|-------|--------| +| persistent | `true` | ✅ | +| targetPVC | `vm-overlay-storage` | ✅ | +| targetHostPath | `null` | ✅ | + +**Overlay Location**: `/var/run/kubevirt-private/vmi-disks/target-/overlay.qcow2` + +**Lifecycle**: +- ✅ Created when VM starts (or reused if exists) +- ✅ Preserved when VM stops +- ✅ Reused when VM restarts +- ✅ Survives pod deletion/recreation + +**Use Cases**: +- Development VMs +- User workstations +- Stateful applications +- VMs that need to preserve changes + +**Storage**: Dedicated PVC (must be created beforehand) + +**Requirements**: +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: vm-overlay-storage +spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: 10Gi +``` + +--- + +### Configuration 3: Persistent with TargetHostPath + +```yaml +overlay: + backingHostPath: + path: /var/lib/images/base.img + targetHostPath: + path: /var/lib/vms/my-vm + backingFormat: raw + persistent: true +``` + +| Field | Value | Result | +|-------|-------|--------| +| persistent | `true` | ✅ | +| targetPVC | `null` | ✅ | +| targetHostPath | `/var/lib/vms/my-vm` | ✅ | + +**Overlay Location**: `/var/lib/vms/my-vm/.qcow2` + +**Lifecycle**: +- ✅ Created when VM starts (or reused if exists) +- ✅ Preserved when VM stops +- ✅ Reused when VM restarts +- ✅ Survives pod deletion +- ⚠️ Tied to specific node (if VM migrates, overlay stays on original node) + +**Use Cases**: +- Local development clusters +- Single-node deployments +- Testing/debugging +- Direct host storage access + +**Storage**: Host filesystem (directory must exist or be creatable) + +**Node Affinity**: VM must run on same node to access overlay + +--- + +## Invalid Configurations + +### Invalid 1: Persistent without Target + +```yaml +overlay: + backingPVC: + claimName: base-image + backingFormat: raw + persistent: true + # No target specified - INVALID! +``` + +| Field | Value | Result | +|-------|-------|--------| +| persistent | `true` | ❌ | +| targetPVC | `null` | ❌ | +| targetHostPath | `null` | ❌ | + +**Error**: `persistent=true requires targetPVC or targetHostPath to be specified. Overlays cannot persist to ephemeral storage.` + +**Why Invalid**: +- Default location uses EmptyDir (deleted when pod terminates) +- Cannot persist data in ephemeral storage +- Would mislead users into thinking data is saved + +**Fix**: Add a target location: +```yaml +targetPVC: + claimName: my-overlay-storage +``` + +--- + +### Invalid 2: Non-Persistent with Target + +```yaml +overlay: + backingPVC: + claimName: base-image + targetPVC: + claimName: vm-overlay-storage + backingFormat: raw + persistent: false # INVALID with target! +``` + +| Field | Value | Result | +|-------|-------|--------| +| persistent | `false` | ❌ | +| targetPVC | `vm-overlay-storage` | ❌ | +| targetHostPath | `null` | ❌ | + +**Error**: `targetPVC or targetHostPath specified requires persistent=true. Non-persistent overlays must use the default ephemeral location.` + +**Why Invalid**: +- Specifying a PVC/HostPath target implies you want to keep the data +- Contradicts the `persistent: false` setting +- Would waste storage resources + +**Fix**: Either: +1. Set `persistent: true` to keep the overlay +2. Remove the target to use ephemeral storage + +--- + +### Invalid 3: Multiple Targets + +```yaml +overlay: + backingPVC: + claimName: base-image + targetPVC: + claimName: pvc-overlay + targetHostPath: + path: /var/lib/overlays # INVALID - can't have both! + persistent: true +``` + +| Field | Value | Result | +|-------|-------|--------| +| persistent | `true` | ✅ | +| targetPVC | `pvc-overlay` | ❌ | +| targetHostPath | `/var/lib/overlays` | ❌ | + +**Error**: `can have at most one target (targetHostPath or targetPVC)` + +**Why Invalid**: +- Overlay can only be stored in one location +- Ambiguous which target to use +- Could lead to data inconsistency + +**Fix**: Choose one target: +```yaml +# Either this: +targetPVC: + claimName: pvc-overlay + +# Or this: +targetHostPath: + path: /var/lib/overlays +``` + +--- + +### Invalid 4: Multiple Backing Sources + +```yaml +overlay: + backingPVC: + claimName: base-pvc + backingHostPath: + path: /var/lib/base.img # INVALID - can't have both! + persistent: true + targetPVC: + claimName: overlay-pvc +``` + +**Error**: `must have exactly one backing source (backingPVC or backingHostPath)` + +**Why Invalid**: +- qemu-img can only have one backing file +- Ambiguous which backing source to use + +**Fix**: Choose one backing source + +--- + +### Invalid 5: No Backing Source + +```yaml +overlay: + # No backing source - INVALID! + targetPVC: + claimName: overlay-pvc + persistent: true +``` + +**Error**: `must have at least one backing source (backingPVC or backingHostPath)` + +**Why Invalid**: +- Overlay must have a backing file to create copy-on-write image +- No base image specified + +**Fix**: Add a backing source: +```yaml +backingPVC: + claimName: base-image +``` + +--- + +## State Transition Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Overlay Volume States │ +└─────────────────────────────────────────────────────────────┘ + + User Configuration + │ + ▼ + ┌─────────────────────────┐ + │ Validation Check │ + └─────────────────────────┘ + │ │ + Valid │ │ Invalid + │ │ + ▼ ▼ + ┌────────────────┐ ┌──────────────┐ + │ Allowed States │ │ Reject & Err │ + └────────────────┘ └──────────────┘ + │ + ┌───────┴───────┬──────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ + │ State 1 │ │ State 2 │ │ State 3 │ + │ │ │ │ │ │ + │ persist │ │ persist │ │ persist │ + │ =false │ │ =true │ │ =true │ + │ │ │ │ │ │ + │ target │ │ target │ │ target │ + │ =none │ │ =PVC │ │ =HostP │ + └─────────┘ └─────────┘ └─────────┘ + │ │ │ + ▼ ▼ ▼ + Ephemeral Persistent Persistent + EmptyDir PVC HostPath +``` + +--- + +## Backing Format States + +Independent of persistent/target settings: + +### State: backingFormat = "raw" (default) + +```yaml +overlay: + backingFormat: raw # or omitted +``` + +- Backing file is raw disk image +- Most common for PVC-based volumes +- Example: ContainerDisk volumes, CDI-imported images + +**qemu-img command**: +```bash +qemu-img create -f qcow2 -b -F raw +``` + +--- + +### State: backingFormat = "qcow2" + +```yaml +overlay: + backingFormat: qcow2 +``` + +- Backing file is already qcow2 +- Common for cloud images, downloaded VMs +- Creates qcow2-on-qcow2 (less common but valid) + +**qemu-img command**: +```bash +qemu-img create -f qcow2 -b -F qcow2 +``` + +--- + +### State: backingFormat = invalid + +```yaml +overlay: + backingFormat: vmdk # INVALID +``` + +**Error**: `invalid backingFormat 'vmdk', allowed values are 'raw' or 'qcow2'` + +**Why Invalid**: Only raw and qcow2 formats are supported + +--- + +## Summary Table + +| persistent | targetPVC | targetHostPath | Valid | Storage Location | Lifecycle | +|-----------|-----------|----------------|-------|------------------|-----------| +| `false` | `null` | `null` | ✅ | EmptyDir (default) | Deleted on VM stop | +| `true` | PVC name | `null` | ✅ | PVC | Survives restarts | +| `true` | `null` | path | ✅ | Host filesystem | Survives restarts | +| `true` | `null` | `null` | ❌ | - | Error: need target for persistence | +| `false` | PVC name | `null` | ❌ | - | Error: target implies persistence | +| `false` | `null` | path | ❌ | - | Error: target implies persistence | +| `true` | PVC name | path | ❌ | - | Error: multiple targets | +| `false` | PVC name | path | ❌ | - | Error: multiple targets | + +--- + +## Decision Tree + +``` +Need overlay volume? +│ +├─ Do you need to keep changes across VM restarts? +│ │ +│ ├─ YES → Set persistent: true +│ │ │ +│ │ ├─ Need portable storage? +│ │ │ └─ YES → Use targetPVC +│ │ │ (works on any node) +│ │ │ +│ │ └─ Local/single node OK? +│ │ └─ YES → Use targetHostPath +│ │ (faster, simpler for dev) +│ │ +│ └─ NO → Set persistent: false +│ └─ Don't specify target +│ (uses ephemeral EmptyDir) +│ +└─ Backing source? + │ + ├─ Base image in PVC? → Use backingPVC + │ + └─ Base image on host? → Use backingHostPath +``` + +--- + +## Real-World Scenarios + +### Scenario 1: CI/CD Test Runner +```yaml +persistent: false +target: none +# Fresh environment for each test run +``` + +### Scenario 2: Developer Workstation +```yaml +persistent: true +targetPVC: dev-workspace-overlay +# Keep changes, survives cluster operations +``` + +### Scenario 3: Production VM (Multi-node cluster) +```yaml +persistent: true +targetPVC: prod-vm-overlay +# Portable, can migrate between nodes +``` + +### Scenario 4: Local Lab/Testing +```yaml +persistent: true +targetHostPath: /lab/vms/test-vm +# Simple, direct host access +``` + +### Scenario 5: Classroom Environment +```yaml +persistent: false +target: none +# Students get fresh VM each session +``` + +--- + +## Migration Between States + +### From Non-Persistent to Persistent + +**Not Possible**: Ephemeral overlays are deleted when VM stops + +**Workaround**: +1. Keep VM running +2. Copy overlay file to persistent location +3. Update VMI spec with new persistent configuration +4. Restart VM with new config + +### From Persistent to Non-Persistent + +**Possible but Destructive**: +1. Change `persistent: true` → `false` +2. Remove target +3. Restart VM +4. Old overlay remains in PVC/HostPath but is no longer used +5. New ephemeral overlay created + +### Changing Target Location + +**Requires Manual Copy**: +1. Stop VM +2. Copy overlay from old target to new target +3. Update VMI spec with new target +4. Start VM + +--- + +## Best Practices + +1. **Development**: Use `persistent: true` + `targetPVC` for flexibility +2. **Testing**: Use `persistent: false` for clean test runs +3. **Production**: Use `persistent: true` + `targetPVC` for reliability +4. **Local Dev**: Use `persistent: true` + `targetHostPath` for simplicity +5. **Shared Bases**: Always use `backingPVC` for multiple VMs from one base +6. **Storage Planning**: Size target PVCs appropriately for expected changes +7. **Cleanup**: Delete unused target PVCs to reclaim storage + diff --git a/pkg/ephemeral-disk/ephemeral-disk.go b/pkg/ephemeral-disk/ephemeral-disk.go index 828ee076be94..a66233b45462 100644 --- a/pkg/ephemeral-disk/ephemeral-disk.go +++ b/pkg/ephemeral-disk/ephemeral-disk.go @@ -101,7 +101,11 @@ func (c *ephemeralDiskCreator) CreateBackedImageForVolume(volume v1.Volume, back imagePath := c.GetFilePath(volume.Name) + // Check if overlay already exists (for persistent overlays that are being reused) if _, err := os.Stat(imagePath); err == nil { + // Overlay exists - for persistent overlays this is expected on VM restart + // For non-persistent overlays, this shouldn't happen (they get cleaned up) + // but we'll still reuse it if it exists return nil } else if !errors.Is(err, os.ErrNotExist) { return err diff --git a/pkg/ephemeral-disk/overlay-disk-handler.go b/pkg/ephemeral-disk/overlay-disk-handler.go new file mode 100644 index 000000000000..a6dc0fc557a0 --- /dev/null +++ b/pkg/ephemeral-disk/overlay-disk-handler.go @@ -0,0 +1,190 @@ +/* + * This file is part of the KubeVirt project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2025 Red Hat, Inc. + * + */ + +package ephemeraldisk + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + v1 "kubevirt.io/api/core/v1" + + diskutils "kubevirt.io/kubevirt/pkg/ephemeral-disk-utils" + "kubevirt.io/kubevirt/pkg/util" + "kubevirt.io/kubevirt/pkg/virt-launcher/virtwrap/api" +) + +const ( + overlayDiskDir = "/var/run/kubevirt-private/overlay-disks" +) + +type OverlayDiskHandlerInterface interface { + CreateOverlayImages(vmi *v1.VirtualMachineInstance, domain *api.Domain) error + CleanupNonPersistentOverlays(vmi *v1.VirtualMachineInstance) error + GetOverlayPath(volumeName string) string +} + +type overlayDiskHandler struct { + baseDir string + pvcBaseDir string + blockDevBaseDir string + diskCreateFunc func(backingFile string, backingFormat string, imagePath string) ([]byte, error) +} + +func NewOverlayDiskHandler() *overlayDiskHandler { + return &overlayDiskHandler{ + baseDir: overlayDiskDir, + pvcBaseDir: ephemeralDiskPVCBaseDir, + blockDevBaseDir: ephemeralDiskBlockDeviceBaseDir, + diskCreateFunc: createBackingDisk, + } +} + +func (h *overlayDiskHandler) GetOverlayPath(volumeName string) string { + return filepath.Join(h.baseDir, volumeName, "disk.qcow2") +} + +func (h *overlayDiskHandler) getTargetPath(overlay *v1.OverlayVolumeSource, volumeName string) string { + if overlay.TargetPVC != nil { + // For target PVC, the volume is mounted at /var/run/kubevirt-private/vmi-disks/target- + targetVolumeName := fmt.Sprintf("target-%s", volumeName) + return filepath.Join(h.pvcBaseDir, targetVolumeName, "overlay.qcow2") + } + + if overlay.TargetHostPath != nil { + // For target HostPath, create the file in the specified directory + // Assume the path is a directory and append the filename + return filepath.Join(overlay.TargetHostPath.Path, fmt.Sprintf("%s.qcow2", volumeName)) + } + + // Default: use ephemeral overlay disk location + return h.GetOverlayPath(volumeName) +} + +func (h *overlayDiskHandler) getBackingInfo(overlay *v1.OverlayVolumeSource, volumeName string, isBlockVolumes map[string]bool) (string, string) { + // Determine backing format (default to raw if not specified) + backingFormat := "raw" + if overlay.BackingFormat != "" { + backingFormat = overlay.BackingFormat + } + + // Determine backing file path + var backingFile string + backingVolumeName := fmt.Sprintf("backing-%s", volumeName) + + if overlay.BackingPVC != nil { + // For PVC backing, use the standard backing file path + if isBlockVolumes[backingVolumeName] { + backingFile = filepath.Join(h.blockDevBaseDir, backingVolumeName) + } else { + backingFile = filepath.Join(h.pvcBaseDir, backingVolumeName, "disk.img") + } + } else if overlay.BackingHostPath != nil { + // For HostPath backing, use the direct path + backingFile = overlay.BackingHostPath.Path + } + + return backingFile, backingFormat +} + +func (h *overlayDiskHandler) createOverlayImage(volumeName string, backingFile string, backingFormat string, targetPath string) error { + // Create the directory for the overlay file + overlayDir := filepath.Dir(targetPath) + if err := util.MkdirAllWithNosec(overlayDir); err != nil { + return err + } + + // Check if overlay already exists (for persistent overlays that are being reused) + if _, err := os.Stat(targetPath); err == nil { + // Overlay exists - for persistent overlays this is expected on VM restart + return nil + } else if !errors.Is(err, os.ErrNotExist) { + return err + } + + // Create the overlay using qemu-img + output, err := h.diskCreateFunc(backingFile, backingFormat, targetPath) + if err != nil { + return fmt.Errorf("qemu-img failed with output '%s': %v", string(output), err) + } + + // Set proper permissions + if err = os.Chmod(targetPath, 0640); err != nil { + return fmt.Errorf("failed to change permissions on %s", targetPath) + } + + // Ensure correct ownership + err = diskutils.DefaultOwnershipManager.UnsafeSetFileOwnership(targetPath) + return err +} + +func (h *overlayDiskHandler) CreateOverlayImages(vmi *v1.VirtualMachineInstance, domain *api.Domain) error { + isBlockVolumes := diskutils.GetEphemeralBackingSourceBlockDevices(domain) + + for _, volume := range vmi.Spec.Volumes { + if volume.VolumeSource.Overlay != nil { + overlay := volume.VolumeSource.Overlay + + // Get backing file information + backingFile, backingFormat := h.getBackingInfo(overlay, volume.Name, isBlockVolumes) + + // Determine the target path for the overlay + targetPath := h.getTargetPath(overlay, volume.Name) + + // Create the overlay image + if err := h.createOverlayImage(volume.Name, backingFile, backingFormat, targetPath); err != nil { + return fmt.Errorf("failed to create overlay for volume %s: %v", volume.Name, err) + } + } + } + + return nil +} + +func (h *overlayDiskHandler) CleanupNonPersistentOverlays(vmi *v1.VirtualMachineInstance) error { + // Clean up overlay volumes that are marked as non-persistent + // This should be called by virt-handler when a VMI is being deleted/stopped + // Note: Due to validation, non-persistent overlays can only use the default location + // (persistent=true requires a target, and target requires persistent=true) + for _, volume := range vmi.Spec.Volumes { + if volume.VolumeSource.Overlay != nil && !volume.VolumeSource.Overlay.Persistent { + overlay := volume.VolumeSource.Overlay + + // Get the actual overlay path (should be default location for non-persistent) + overlayPath := h.getTargetPath(overlay, volume.Name) + + // Check if overlay file exists before trying to delete + if _, err := os.Stat(overlayPath); err == nil { + // Remove the overlay file + if err := os.Remove(overlayPath); err != nil { + return fmt.Errorf("failed to remove non-persistent overlay %s: %v", overlayPath, err) + } + } else if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to stat overlay file %s: %v", overlayPath, err) + } + + // Try to remove the directory if it's empty + overlayDir := filepath.Dir(overlayPath) + os.Remove(overlayDir) // Ignore error - directory might not be empty or already gone + } + } + return nil +} diff --git a/pkg/ephemeral-disk/overlay-disk-handler_test.go b/pkg/ephemeral-disk/overlay-disk-handler_test.go new file mode 100644 index 000000000000..e248f40f8c88 --- /dev/null +++ b/pkg/ephemeral-disk/overlay-disk-handler_test.go @@ -0,0 +1,482 @@ +/* + * This file is part of the KubeVirt project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2025 Red Hat, Inc. + * + */ + +package ephemeraldisk + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + k8sv1 "k8s.io/api/core/v1" + + v1 "kubevirt.io/api/core/v1" + + "kubevirt.io/kubevirt/pkg/libvmi" + "kubevirt.io/kubevirt/pkg/virt-launcher/virtwrap/api" +) + +var _ = Describe("OverlayDiskHandler", func() { + var overlayBaseDir string + var pvcBaseDir string + var blockDevBaseDir string + var targetHostPathDir string + var handler *overlayDiskHandler + + createBackingImageForPVC := func(volumeName string, isBlock bool) error { + backingVolumeName := fmt.Sprintf("backing-%s", volumeName) + if err := os.Mkdir(filepath.Join(pvcBaseDir, backingVolumeName), 0755); err != nil { + return err + } + var backingPath string + if isBlock { + backingPath = filepath.Join(blockDevBaseDir, backingVolumeName) + } else { + backingPath = filepath.Join(pvcBaseDir, backingVolumeName, "disk.img") + } + f, err := os.Create(backingPath) + if err != nil { + return err + } + defer f.Close() + return nil + } + + createBackingImageForHostPath := func(hostPath string) error { + dir := filepath.Dir(hostPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + f, err := os.Create(hostPath) + if err != nil { + return err + } + defer f.Close() + return nil + } + + BeforeEach(func() { + overlayBaseDir = GinkgoT().TempDir() + pvcBaseDir = GinkgoT().TempDir() + blockDevBaseDir = GinkgoT().TempDir() + targetHostPathDir = GinkgoT().TempDir() + + handler = &overlayDiskHandler{ + baseDir: overlayBaseDir, + pvcBaseDir: pvcBaseDir, + blockDevBaseDir: blockDevBaseDir, + diskCreateFunc: fakeCreateOverlayDisk, + } + }) + + Describe("overlay volume with PVC backing", func() { + Context("With single overlay volume and default target", func() { + It("Should create overlay image in default location", func() { + By("Creating a minimal VMI with overlay volume backed by PVC") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc", + }, + }, + BackingFormat: "raw", + Persistent: false, + }, + }, + }, + } + + By("Creating a backing image for the PVC") + Expect(createBackingImageForPVC("overlay-disk", false)).To(Succeed()) + + By("Creating overlay image") + err := handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying overlay exists in default location") + expectedPath := filepath.Join(overlayBaseDir, "overlay-disk", "disk.qcow2") + _, err = os.Stat(expectedPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("With overlay volume and TargetPVC", func() { + It("Should create overlay image in target PVC location", func() { + By("Creating a minimal VMI with overlay volume and target PVC") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc", + }, + }, + TargetPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "target-pvc", + }, + }, + BackingFormat: "raw", + Persistent: true, + }, + }, + }, + } + + By("Creating a backing image for the PVC") + Expect(createBackingImageForPVC("overlay-disk", false)).To(Succeed()) + + By("Creating overlay image") + err := handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying overlay exists in target PVC location") + expectedPath := filepath.Join(pvcBaseDir, "target-overlay-disk", "overlay.qcow2") + _, err = os.Stat(expectedPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("With block PVC backing", func() { + It("Should create overlay with block backing source", func() { + By("Creating a minimal VMI with overlay volume backed by block PVC") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc", + }, + }, + BackingFormat: "raw", + }, + }, + }, + } + + By("Creating a backing block device for the PVC") + Expect(createBackingImageForPVC("overlay-disk", true)).To(Succeed()) + + By("Creating overlay image with block backing") + err := handler.CreateOverlayImages(vmi, &api.Domain{ + Spec: api.DomainSpec{ + Devices: api.Devices{ + Disks: []api.Disk{ + { + BackingStore: &api.BackingStore{ + Type: "block", + Source: &api.DiskSource{ + Dev: filepath.Join(blockDevBaseDir, "backing-overlay-disk"), + Name: "backing-overlay-disk", + }, + }, + }, + }, + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying overlay exists") + expectedPath := filepath.Join(overlayBaseDir, "overlay-disk", "disk.qcow2") + _, err = os.Stat(expectedPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("overlay volume with HostPath backing", func() { + Context("With HostPath backing and default target", func() { + It("Should create overlay image with HostPath backing", func() { + backingPath := filepath.Join(GinkgoT().TempDir(), "base-image.img") + By("Creating a backing image at HostPath") + Expect(createBackingImageForHostPath(backingPath)).To(Succeed()) + + By("Creating a minimal VMI with overlay volume backed by HostPath") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingHostPath: &k8sv1.HostPathVolumeSource{ + Path: backingPath, + }, + BackingFormat: "qcow2", + }, + }, + }, + } + + By("Creating overlay image") + err := handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying overlay exists in default location") + expectedPath := filepath.Join(overlayBaseDir, "overlay-disk", "disk.qcow2") + _, err = os.Stat(expectedPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("With HostPath backing and TargetHostPath", func() { + It("Should create overlay image at target HostPath", func() { + backingPath := filepath.Join(GinkgoT().TempDir(), "base-image.img") + By("Creating a backing image at HostPath") + Expect(createBackingImageForHostPath(backingPath)).To(Succeed()) + + By("Creating a minimal VMI with overlay volume and target HostPath") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingHostPath: &k8sv1.HostPathVolumeSource{ + Path: backingPath, + }, + TargetHostPath: &k8sv1.HostPathVolumeSource{ + Path: targetHostPathDir, + }, + BackingFormat: "qcow2", + Persistent: true, + }, + }, + }, + } + + By("Creating overlay image") + err := handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying overlay exists at target HostPath") + expectedPath := filepath.Join(targetHostPathDir, "overlay-disk.qcow2") + _, err = os.Stat(expectedPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("multiple overlay volumes", func() { + It("Should create multiple overlay images", func() { + By("Creating a VMI with multiple overlay volumes") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk1", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc1", + }, + }, + BackingFormat: "raw", + }, + }, + }, + { + Name: "overlay-disk2", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc2", + }, + }, + BackingFormat: "raw", + }, + }, + }, + } + + By("Creating backing images for the PVCs") + Expect(createBackingImageForPVC("overlay-disk1", false)).To(Succeed()) + Expect(createBackingImageForPVC("overlay-disk2", false)).To(Succeed()) + + By("Creating overlay images") + err := handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying all overlays exist") + _, err = os.Stat(filepath.Join(overlayBaseDir, "overlay-disk1", "disk.qcow2")) + Expect(err).NotTo(HaveOccurred()) + _, err = os.Stat(filepath.Join(overlayBaseDir, "overlay-disk2", "disk.qcow2")) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("idempotency", func() { + It("Should handle creating overlays idempotently", func() { + By("Creating a VMI with overlay volume") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc", + }, + }, + BackingFormat: "raw", + }, + }, + }, + } + + By("Creating backing image") + Expect(createBackingImageForPVC("overlay-disk", false)).To(Succeed()) + + By("Creating overlay image first time") + err := handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating overlay image second time (should be idempotent)") + err = handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying overlay still exists") + expectedPath := filepath.Join(overlayBaseDir, "overlay-disk", "disk.qcow2") + _, err = os.Stat(expectedPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("cleanup non-persistent overlays", func() { + It("Should cleanup non-persistent overlays", func() { + By("Creating a VMI with non-persistent overlay") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc", + }, + }, + BackingFormat: "raw", + Persistent: false, + }, + }, + }, + } + + By("Creating backing image and overlay") + Expect(createBackingImageForPVC("overlay-disk", false)).To(Succeed()) + err := handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + overlayPath := filepath.Join(overlayBaseDir, "overlay-disk", "disk.qcow2") + By("Verifying overlay exists") + _, err = os.Stat(overlayPath) + Expect(err).NotTo(HaveOccurred()) + + By("Cleaning up non-persistent overlays") + err = handler.CleanupNonPersistentOverlays(vmi) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying overlay was deleted") + _, err = os.Stat(overlayPath) + Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue()) + }) + + It("Should not cleanup persistent overlays", func() { + backingPath := filepath.Join(GinkgoT().TempDir(), "base-image.img") + By("Creating backing image at HostPath") + Expect(createBackingImageForHostPath(backingPath)).To(Succeed()) + + By("Creating a VMI with persistent overlay") + vmi := libvmi.New() + vmi.Spec.Volumes = []v1.Volume{ + { + Name: "overlay-disk", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingHostPath: &k8sv1.HostPathVolumeSource{ + Path: backingPath, + }, + TargetHostPath: &k8sv1.HostPathVolumeSource{ + Path: targetHostPathDir, + }, + BackingFormat: "qcow2", + Persistent: true, + }, + }, + }, + } + + By("Creating overlay image") + err := handler.CreateOverlayImages(vmi, &api.Domain{}) + Expect(err).NotTo(HaveOccurred()) + + overlayPath := filepath.Join(targetHostPathDir, "overlay-disk.qcow2") + By("Verifying overlay exists") + _, err = os.Stat(overlayPath) + Expect(err).NotTo(HaveOccurred()) + + By("Attempting to cleanup (should skip persistent overlays)") + err = handler.CleanupNonPersistentOverlays(vmi) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying persistent overlay still exists") + _, err = os.Stat(overlayPath) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) + +func fakeCreateOverlayDisk(backingFile string, backingFormat string, imagePath string) ([]byte, error) { + // Validate backing format + if backingFormat != "raw" && backingFormat != "qcow2" { + return nil, fmt.Errorf("invalid backing format: %s", backingFormat) + } + + // Check if backing file exists + _, err := os.Stat(backingFile) + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("backing file does not exist: %s", backingFile) + } + + // Create the overlay image file + f, err := os.Create(imagePath) + if err != nil { + return nil, err + } + err = f.Close() + return nil, err +} diff --git a/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go b/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go index c8d045510bb4..3ad3f4104645 100644 --- a/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go +++ b/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter.go @@ -1612,6 +1612,9 @@ func validateVolumes(field *k8sfield.Path, volumes []v1.Volume, config *virtconf if volume.Ephemeral != nil { volumeSourceSetCount++ } + if volume.Overlay != nil { + volumeSourceSetCount++ + } if volume.EmptyDisk != nil { volumeSourceSetCount++ } @@ -1829,6 +1832,113 @@ func validateVolumes(field *k8sfield.Path, volumes []v1.Volume, config *virtconf } } + // Validate overlay volume + if overlay := volume.Overlay; overlay != nil { + // Verify at least one backing source is set + backingSourceCount := 0 + if overlay.BackingPVC != nil { + backingSourceCount++ + } + if overlay.BackingHostPath != nil { + backingSourceCount++ + } + + if backingSourceCount == 0 { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Message: fmt.Sprintf("%s must have at least one backing source (backingPVC or backingHostPath)", field.Index(idx).Child("overlay").String()), + Field: field.Index(idx).Child("overlay").String(), + }) + } + + if backingSourceCount > 1 { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Message: fmt.Sprintf("%s must have exactly one backing source (backingPVC or backingHostPath)", field.Index(idx).Child("overlay").String()), + Field: field.Index(idx).Child("overlay").String(), + }) + } + + // Verify at most one target is set (or none for default) + targetCount := 0 + if overlay.TargetHostPath != nil { + targetCount++ + } + if overlay.TargetPVC != nil { + targetCount++ + } + + if targetCount > 1 { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Message: fmt.Sprintf("%s can have at most one target (targetHostPath or targetPVC)", field.Index(idx).Child("overlay").String()), + Field: field.Index(idx).Child("overlay").String(), + }) + } else { + + // Validate persistent flag consistency with target location + if overlay.Persistent && targetCount == 0 { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Message: fmt.Sprintf("%s with persistent=true requires targetPVC or targetHostPath to be specified. Overlays cannot persist to ephemeral storage.", field.Index(idx).Child("overlay").String()), + Field: field.Index(idx).Child("overlay", "persistent").String(), + }) + } + + // If target is specified, persistent must be true + if targetCount > 0 && !overlay.Persistent { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Message: fmt.Sprintf("%s with targetPVC or targetHostPath specified requires persistent=true. Non-persistent overlays must use the default ephemeral location.", field.Index(idx).Child("overlay").String()), + Field: field.Index(idx).Child("overlay", "persistent").String(), + }) + } + } + + // Validate backingFormat if specified + if overlay.BackingFormat != "" && overlay.BackingFormat != "raw" && overlay.BackingFormat != "qcow2" { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Message: fmt.Sprintf("%s has invalid backingFormat '%s', allowed values are 'raw' or 'qcow2'", field.Index(idx).Child("overlay", "backingFormat").String(), overlay.BackingFormat), + Field: field.Index(idx).Child("overlay", "backingFormat").String(), + }) + } + + // Validate PVC name if specified + if overlay.BackingPVC != nil && overlay.BackingPVC.ClaimName == "" { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueRequired, + Message: fmt.Sprintf("%s claimName must be set", field.Index(idx).Child("overlay", "backingPVC").String()), + Field: field.Index(idx).Child("overlay", "backingPVC", "claimName").String(), + }) + } + + if overlay.TargetPVC != nil && overlay.TargetPVC.ClaimName == "" { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueRequired, + Message: fmt.Sprintf("%s claimName must be set", field.Index(idx).Child("overlay", "targetPVC").String()), + Field: field.Index(idx).Child("overlay", "targetPVC", "claimName").String(), + }) + } + + // Validate HostPath if specified + if overlay.BackingHostPath != nil && overlay.BackingHostPath.Path == "" { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueRequired, + Message: fmt.Sprintf("%s path must be set", field.Index(idx).Child("overlay", "backingHostPath").String()), + Field: field.Index(idx).Child("overlay", "backingHostPath", "path").String(), + }) + } + + if overlay.TargetHostPath != nil && overlay.TargetHostPath.Path == "" { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueRequired, + Message: fmt.Sprintf("%s path must be set", field.Index(idx).Child("overlay", "targetHostPath").String()), + Field: field.Index(idx).Child("overlay", "targetHostPath", "path").String(), + }) + } + } + if volume.ServiceAccount != nil { if volume.ServiceAccount.ServiceAccountName == "" { causes = append(causes, metav1.StatusCause{ diff --git a/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go b/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go index 167fd28e37f4..bfee1c3e5f4a 100644 --- a/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go +++ b/pkg/virt-api/webhooks/validating-webhook/admitters/vmi-create-admitter_test.go @@ -2691,6 +2691,229 @@ var _ = Describe("Validating VMICreate Admitter", func() { Expect(causes).To(HaveLen(1)) Expect(causes[0].Message).To(ContainSubstring("fake must have max one memory dump volume set")) }) + It("should accept overlay volume with backing PVC", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "test-pvc", + }, + }, + BackingFormat: "raw", + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(BeEmpty()) + }) + + It("should accept overlay volume with backing HostPath", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingHostPath: &k8sv1.HostPathVolumeSource{ + Path: "/var/lib/images/base.img", + }, + BackingFormat: "qcow2", + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(BeEmpty()) + }) + + It("should accept overlay volume with target PVC", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc", + }, + }, + TargetPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "target-pvc", + }, + }, + BackingFormat: "raw", + Persistent: true, + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(BeEmpty()) + }) + + It("should reject overlay volume without backing source", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{}, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(HaveLen(1)) + Expect(causes[0].Message).To(ContainSubstring("must have at least one backing source")) + }) + + It("should reject overlay volume with multiple backing sources", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "test-pvc", + }, + }, + BackingHostPath: &k8sv1.HostPathVolumeSource{ + Path: "/var/lib/images/base.img", + }, + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(HaveLen(1)) + Expect(causes[0].Message).To(ContainSubstring("must have exactly one backing source")) + }) + + It("should reject overlay volume with multiple target sources", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc", + }, + }, + TargetPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "target-pvc", + }, + }, + TargetHostPath: &k8sv1.HostPathVolumeSource{ + Path: "/var/lib/kubevirt/overlays", + }, + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(HaveLen(1)) + Expect(causes[0].Message).To(ContainSubstring("can have at most one target")) + }) + + It("should reject overlay volume with invalid backing format", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "test-pvc", + }, + }, + BackingFormat: "invalid", + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(HaveLen(1)) + Expect(causes[0].Message).To(ContainSubstring("invalid backingFormat")) + }) + + It("should reject overlay volume with empty backing PVC claimName", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "", + }, + }, + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(HaveLen(1)) + Expect(causes[0].Message).To(ContainSubstring("claimName must be set")) + }) + + It("should reject overlay volume with empty backing HostPath path", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingHostPath: &k8sv1.HostPathVolumeSource{ + Path: "", + }, + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(HaveLen(1)) + Expect(causes[0].Message).To(ContainSubstring("path must be set")) + }) + + It("should reject overlay volume with persistent=true but no target", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "test-pvc", + }, + }, + Persistent: true, + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(HaveLen(1)) + Expect(causes[0].Message).To(ContainSubstring("persistent=true requires targetPVC or targetHostPath")) + }) + + It("should reject overlay volume with target but persistent=false", func() { + vmi := api.NewMinimalVMI("testvmi") + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "testOverlay", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backing-pvc", + }, + }, + TargetPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "target-pvc", + }, + }, + Persistent: false, + }, + }, + }) + causes := validateVolumes(k8sfield.NewPath("fake"), vmi.Spec.Volumes, config) + Expect(causes).To(HaveLen(1)) + Expect(causes[0].Message).To(ContainSubstring("requires persistent=true")) + }) }) diff --git a/pkg/virt-controller/services/rendervolumes.go b/pkg/virt-controller/services/rendervolumes.go index f80c7e5ca12c..aea68b1e5871 100644 --- a/pkg/virt-controller/services/rendervolumes.go +++ b/pkg/virt-controller/services/rendervolumes.go @@ -145,6 +145,12 @@ func withVMIVolumes(pvcStore cache.Store, vmiSpecVolumes []v1.Volume, vmiVolumeS } } + if volume.Overlay != nil { + if err := renderer.handleOverlayVolume(volume, pvcStore); err != nil { + return err + } + } + if volume.HostDisk != nil { renderer.handleHostDisk(volume) } @@ -589,6 +595,107 @@ func (vr *VolumeRenderer) handleEphemeralVolume(volume v1.Volume, pvcStore cache return nil } +func (vr *VolumeRenderer) handleOverlayVolume(volume v1.Volume, pvcStore cache.Store) error { + overlay := volume.Overlay + + // Handle backing source - mount as backing- + backingVolumeName := fmt.Sprintf("backing-%s", volume.Name) + if overlay.BackingPVC != nil { + claimName := overlay.BackingPVC.ClaimName + + // Create a temporary volume struct with the backing volume name for mount path resolution + backingVolume := v1.Volume{ + Name: backingVolumeName, + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: claimName, + ReadOnly: true, + }, + }, + }, + } + + if err := vr.addPVCToLaunchManifest(pvcStore, backingVolume, claimName); err != nil { + return err + } + + vr.podVolumes = append(vr.podVolumes, k8sv1.Volume{ + Name: backingVolumeName, + VolumeSource: k8sv1.VolumeSource{ + PersistentVolumeClaim: &k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: claimName, + ReadOnly: true, // Backing source is always read-only + }, + }, + }) + } else if overlay.BackingHostPath != nil { + hostPathType := k8sv1.HostPathFile + vr.podVolumes = append(vr.podVolumes, k8sv1.Volume{ + Name: backingVolumeName, + VolumeSource: k8sv1.VolumeSource{ + HostPath: &k8sv1.HostPathVolumeSource{ + Path: overlay.BackingHostPath.Path, + Type: &hostPathType, + }, + }, + }) + vr.podVolumeMounts = append(vr.podVolumeMounts, k8sv1.VolumeMount{ + Name: backingVolumeName, + MountPath: overlay.BackingHostPath.Path, + ReadOnly: true, + }) + } + + // Handle target location for overlay + if overlay.TargetPVC != nil { + // Target is a PVC + targetVolumeName := fmt.Sprintf("target-%s", volume.Name) + claimName := overlay.TargetPVC.ClaimName + + // Create a temporary volume struct with the target volume name for mount path resolution + targetVolume := v1.Volume{ + Name: targetVolumeName, + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: claimName, + }, + }, + }, + } + + if err := vr.addPVCToLaunchManifest(pvcStore, targetVolume, claimName); err != nil { + return err + } + + vr.podVolumes = append(vr.podVolumes, k8sv1.Volume{ + Name: targetVolumeName, + VolumeSource: k8sv1.VolumeSource{ + PersistentVolumeClaim: &k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: claimName, + }, + }, + }) + } else if overlay.TargetHostPath != nil { + // Target is a HostPath + targetVolumeName := fmt.Sprintf("target-%s", volume.Name) + hostPathType := k8sv1.HostPathDirectoryOrCreate + vr.podVolumes = append(vr.podVolumes, k8sv1.Volume{ + Name: targetVolumeName, + VolumeSource: k8sv1.VolumeSource{ + HostPath: &k8sv1.HostPathVolumeSource{ + Path: overlay.TargetHostPath.Path, + Type: &hostPathType, + }, + }, + }) + } + // If neither target is specified, we'll use the default location in ephemeral-disks + + return nil +} + func (vr *VolumeRenderer) handleDataVolume(volume v1.Volume, pvcStore cache.Store) error { claimName := volume.DataVolume.Name if err := vr.addPVCToLaunchManifest(pvcStore, volume, claimName); err != nil { diff --git a/pkg/virt-handler/BUILD.bazel b/pkg/virt-handler/BUILD.bazel index e67b63d86c18..9a132dc9011f 100644 --- a/pkg/virt-handler/BUILD.bazel +++ b/pkg/virt-handler/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "//pkg/config:go_default_library", "//pkg/container-disk:go_default_library", "//pkg/controller:go_default_library", + "//pkg/ephemeral-disk:go_default_library", "//pkg/ephemeral-disk-utils:go_default_library", "//pkg/executor:go_default_library", "//pkg/handler-launcher-com/cmd/v1:go_default_library", diff --git a/pkg/virt-handler/vm.go b/pkg/virt-handler/vm.go index 1d4cc4456864..cdf878df16f7 100644 --- a/pkg/virt-handler/vm.go +++ b/pkg/virt-handler/vm.go @@ -59,6 +59,7 @@ import ( "kubevirt.io/kubevirt/pkg/config" containerdisk "kubevirt.io/kubevirt/pkg/container-disk" "kubevirt.io/kubevirt/pkg/controller" + ephemeraldisk "kubevirt.io/kubevirt/pkg/ephemeral-disk" diskutils "kubevirt.io/kubevirt/pkg/ephemeral-disk-utils" "kubevirt.io/kubevirt/pkg/executor" cmdv1 "kubevirt.io/kubevirt/pkg/handler-launcher-com/cmd/v1" @@ -2172,6 +2173,13 @@ func (c *VirtualMachineController) processVmCleanup(vmi *v1.VirtualMachineInstan return err } + // Clean up non-persistent overlay volumes + // This is done on a best-effort basis - we log errors but don't fail the entire cleanup + if err := ephemeraldisk.NewOverlayDiskHandler().CleanupNonPersistentOverlays(vmi); err != nil { + log.Log.Object(vmi).Reason(err).Error("Failed to cleanup non-persistent overlay volumes") + // Continue with cleanup even if overlay cleanup fails + } + // UnmountAll does the cleanup on the "best effort" basis: it is // safe to pass a nil cgroupManager. cgroupManager, _ := getCgroupManager(vmi) diff --git a/pkg/virt-launcher/virtwrap/converter/converter.go b/pkg/virt-launcher/virtwrap/converter/converter.go index eda75b6de226..8640f3bc179d 100644 --- a/pkg/virt-launcher/virtwrap/converter/converter.go +++ b/pkg/virt-launcher/virtwrap/converter/converter.go @@ -576,6 +576,9 @@ func Convert_v1_Volume_To_api_Disk(source *v1.Volume, disk *api.Disk, c *Convert if source.Ephemeral != nil { return Convert_v1_EphemeralVolumeSource_To_api_Disk(source.Name, disk, c) } + if source.Overlay != nil { + return Convert_v1_OverlayVolumeSource_To_api_Disk(source.Name, source.Overlay, disk, c) + } if source.EmptyDisk != nil { return Convert_v1_EmptyDiskSource_To_api_Disk(source.Name, source.EmptyDisk, disk) } @@ -855,6 +858,67 @@ func Convert_v1_EphemeralVolumeSource_To_api_Disk(volumeName string, disk *api.D return nil } +func Convert_v1_OverlayVolumeSource_To_api_Disk(volumeName string, overlay *v1.OverlayVolumeSource, disk *api.Disk, c *ConverterContext) error { + disk.Type = "file" + disk.Driver.Type = "qcow2" + disk.Driver.ErrorPolicy = v1.DiskErrorPolicyStop + disk.Driver.Discard = "unmap" + + // Set the overlay disk path based on target location + disk.Source.File = getOverlayDiskPath(volumeName, overlay) + + // Configure backing store + disk.BackingStore = &api.BackingStore{ + Format: &api.BackingStoreFormat{}, + Source: &api.DiskSource{}, + } + + // Determine backing format (default to raw if not specified) + backingFormat := "raw" + if overlay.BackingFormat != "" { + backingFormat = overlay.BackingFormat + } + disk.BackingStore.Format.Type = backingFormat + + // Set backing source path based on backing source type + backingVolumeName := fmt.Sprintf("backing-%s", volumeName) + if overlay.BackingPVC != nil { + // For PVC backing, check if it's block or filesystem + if c.IsBlockPVC[backingVolumeName] { + disk.BackingStore.Type = "block" + disk.BackingStore.Source.Dev = GetBlockDeviceVolumePath(backingVolumeName) + } else { + disk.BackingStore.Type = "file" + disk.BackingStore.Source.File = GetFilesystemVolumePath(backingVolumeName) + } + } else if overlay.BackingHostPath != nil { + // For HostPath backing + disk.BackingStore.Type = "file" + disk.BackingStore.Source.File = overlay.BackingHostPath.Path + } + + return nil +} + +func getOverlayDiskPath(volumeName string, overlay *v1.OverlayVolumeSource) string { + if overlay.TargetPVC != nil { + // For target PVC, the volume is mounted at /var/run/kubevirt-private/vmi-disks/target- + targetVolumeName := fmt.Sprintf("target-%s", volumeName) + return filepath.Join(string(filepath.Separator), "var", "run", "kubevirt-private", "vmi-disks", targetVolumeName, "overlay.qcow2") + } + + if overlay.TargetHostPath != nil { + // For target HostPath, the file is created directly at the specified path + path := overlay.TargetHostPath.Path + // Append filename to the path (assume it's a directory) + return filepath.Join(path, fmt.Sprintf("%s.qcow2", volumeName)) + } + + // Default: use overlay disk location + // /var/run/kubevirt-private/overlay-disks//disk.qcow2 + return filepath.Join(string(filepath.Separator), "var", "run", "kubevirt-private", "overlay-disks", volumeName, "disk.qcow2") +} + func Convert_v1_Watchdog_To_api_Watchdog(source *v1.Watchdog, watchdog *api.Watchdog, _ *ConverterContext) error { watchdog.Alias = api.NewUserDefinedAlias(source.Name) if source.I6300ESB != nil { diff --git a/pkg/virt-launcher/virtwrap/converter/converter_test.go b/pkg/virt-launcher/virtwrap/converter/converter_test.go index b371879bf819..7e4c7ecf136d 100644 --- a/pkg/virt-launcher/virtwrap/converter/converter_test.go +++ b/pkg/virt-launcher/virtwrap/converter/converter_test.go @@ -3356,6 +3356,7 @@ var _ = Describe("SetDriverCacheMode", func() { var ctrl *gomock.Controller var mockDirectIOChecker *MockDirectIOChecker + EphemeralDiskImageCreator := &fake.MockEphemeralDiskImageCreator{BaseDir: "/var/run/libvirt/kubevirt-ephemeral-disk/"} BeforeEach(func() { ctrl = gomock.NewController(GinkgoT()) mockDirectIOChecker = NewMockDirectIOChecker(ctrl) @@ -3405,6 +3406,118 @@ var _ = Describe("SetDriverCacheMode", func() { Entry("'writethrough' without direct io", string(v1.CacheWriteThrough), string(v1.CacheWriteThrough), expectCheckFalse), Entry("'writethrough' on error", string(v1.CacheWriteThrough), string(v1.CacheWriteThrough), expectCheckError), ) + + Context("with overlay volumes", func() { + It("should convert overlay volume with backing PVC", func() { + vmi := kvapi.NewMinimalVMI("testvmi") + vmi.Spec.Domain.Devices.Disks = append(vmi.Spec.Domain.Devices.Disks, v1.Disk{ + Name: "test", + }) + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "test", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "test-pvc", + }, + }, + BackingFormat: "raw", + }, + }, + }) + + isBlockPVCMap := make(map[string]bool) + isBlockPVCMap["backing-test"] = false + + domain := vmiToDomain(vmi, &ConverterContext{ + Architecture: archconverter.NewConverter(runtime.GOARCH), + AllowEmulation: true, + EphemeraldiskCreator: EphemeralDiskImageCreator, + IsBlockPVC: isBlockPVCMap, + IsBlockDV: make(map[string]bool), + }) + + Expect(domain.Spec.Devices.Disks).To(HaveLen(1)) + Expect(domain.Spec.Devices.Disks[0].Type).To(Equal("file")) + Expect(domain.Spec.Devices.Disks[0].Driver.Type).To(Equal("qcow2")) + Expect(domain.Spec.Devices.Disks[0].BackingStore).ToNot(BeNil()) + Expect(domain.Spec.Devices.Disks[0].BackingStore.Format.Type).To(Equal("raw")) + Expect(domain.Spec.Devices.Disks[0].BackingStore.Type).To(Equal("file")) + }) + + It("shoujld convert overlay volume with backing HostPath", func() { + vmi := kvapi.NewMinimalVMI("testvmi") + vmi.Spec.Domain.Devices.Disks = append(vmi.Spec.Domain.Devices.Disks, v1.Disk{ + Name: "test", + }) + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "test", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingHostPath: &k8sv1.HostPathVolumeSource{ + Path: "/var/lib/images/base.img", + }, + BackingFormat: "qcow2", + }, + }, + }) + + domain := vmiToDomain(vmi, &ConverterContext{ + Architecture: archconverter.NewConverter(runtime.GOARCH), + AllowEmulation: true, + EphemeraldiskCreator: EphemeralDiskImageCreator, + IsBlockPVC: make(map[string]bool), + IsBlockDV: make(map[string]bool), + }) + + Expect(domain.Spec.Devices.Disks).To(HaveLen(1)) + Expect(domain.Spec.Devices.Disks[0].Type).To(Equal("file")) + Expect(domain.Spec.Devices.Disks[0].Driver.Type).To(Equal("qcow2")) + Expect(domain.Spec.Devices.Disks[0].BackingStore).ToNot(BeNil()) + Expect(domain.Spec.Devices.Disks[0].BackingStore.Format.Type).To(Equal("qcow2")) + Expect(domain.Spec.Devices.Disks[0].BackingStore.Type).To(Equal("file")) + Expect(domain.Spec.Devices.Disks[0].BackingStore.Source.File).To(Equal("/var/lib/images/base.img")) + }) + + It("should convert overlay volume with block PVC backing", func() { + vmi := kvapi.NewMinimalVMI("testvmi") + vmi.Spec.Domain.Devices.Disks = append(vmi.Spec.Domain.Devices.Disks, v1.Disk{ + Name: "test", + }) + vmi.Spec.Volumes = append(vmi.Spec.Volumes, v1.Volume{ + Name: "test", + VolumeSource: v1.VolumeSource{ + Overlay: &v1.OverlayVolumeSource{ + BackingPVC: &v1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: "block-pvc", + }, + }, + BackingFormat: "raw", + }, + }, + }) + + isBlockPVCMap := make(map[string]bool) + isBlockPVCMap["backing-test"] = true + + domain := vmiToDomain(vmi, &ConverterContext{ + Architecture: archconverter.NewConverter(runtime.GOARCH), + AllowEmulation: true, + EphemeraldiskCreator: EphemeralDiskImageCreator, + IsBlockPVC: isBlockPVCMap, + IsBlockDV: make(map[string]bool), + }) + + Expect(domain.Spec.Devices.Disks).To(HaveLen(1)) + Expect(domain.Spec.Devices.Disks[0].Type).To(Equal("file")) + Expect(domain.Spec.Devices.Disks[0].Driver.Type).To(Equal("qcow2")) + Expect(domain.Spec.Devices.Disks[0].BackingStore).ToNot(BeNil()) + Expect(domain.Spec.Devices.Disks[0].BackingStore.Type).To(Equal("block")) + Expect(domain.Spec.Devices.Disks[0].BackingStore.Source.Dev).ToNot(BeEmpty()) + }) + }) }) func diskToDiskXML(arch string, disk *v1.Disk) string { diff --git a/pkg/virt-launcher/virtwrap/manager.go b/pkg/virt-launcher/virtwrap/manager.go index 553ce7f3bd87..19c45ee4934d 100644 --- a/pkg/virt-launcher/virtwrap/manager.go +++ b/pkg/virt-launcher/virtwrap/manager.go @@ -793,6 +793,11 @@ func (l *LibvirtDomainManager) preStartHook(vmi *v1.VirtualMachineInstance, doma if err != nil { return domain, fmt.Errorf("preparing ephemeral images failed: %v", err) } + // Create images for overlay volumes. + err = ephemeraldisk.NewOverlayDiskHandler().CreateOverlayImages(vmi, domain) + if err != nil { + return domain, fmt.Errorf("preparing overlay images failed: %v", err) + } // create empty disks if they exist if err := emptydisk.NewEmptyDiskCreator().CreateTemporaryDisks(vmi); err != nil { return domain, fmt.Errorf("creating empty disks failed: %v", err) diff --git a/pkg/virt-operator/resource/generate/components/validations_generated.go b/pkg/virt-operator/resource/generate/components/validations_generated.go index d1d38b590473..45270cdba322 100644 --- a/pkg/virt-operator/resource/generate/components/validations_generated.go +++ b/pkg/virt-operator/resource/generate/components/validations_generated.go @@ -7890,6 +7890,105 @@ var CRDsValidation map[string]string = map[string]string{ Must be a DNS_LABEL and unique within the vmi. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string + overlay: + description: |- + Overlay represents a qcow2 overlay volume with explicit backing and target sources. + Provides copy-on-write semantics with more control than ephemeral volumes. + properties: + backingFormat: + description: |- + BackingFormat specifies the format of the backing image (raw or qcow2). + Defaults to raw if not specified. + type: string + backingHostPath: + description: BackingHostPath represents a pre-existing + host file or directory used as the backing source. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + backingPVC: + description: BackingPVC is a reference to a PersistentVolumeClaim + used as the backing (read-only) source. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether the volume + can be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + persistent: + description: |- + Persistent indicates whether the overlay should be kept across VM restarts. + If false, the overlay is deleted when the VM stops. + Defaults to false. + type: boolean + targetHostPath: + description: |- + TargetHostPath specifies where to place the overlay file on the host. + If empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks). + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + targetPVC: + description: |- + TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored. + If specified, the overlay will be persisted to this PVC instead of node-local storage. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether the volume + can be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + type: object persistentVolumeClaim: description: |- PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. @@ -13090,6 +13189,105 @@ var CRDsValidation map[string]string = map[string]string{ Must be a DNS_LABEL and unique within the vmi. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string + overlay: + description: |- + Overlay represents a qcow2 overlay volume with explicit backing and target sources. + Provides copy-on-write semantics with more control than ephemeral volumes. + properties: + backingFormat: + description: |- + BackingFormat specifies the format of the backing image (raw or qcow2). + Defaults to raw if not specified. + type: string + backingHostPath: + description: BackingHostPath represents a pre-existing host file + or directory used as the backing source. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + backingPVC: + description: BackingPVC is a reference to a PersistentVolumeClaim + used as the backing (read-only) source. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether the volume can + be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + persistent: + description: |- + Persistent indicates whether the overlay should be kept across VM restarts. + If false, the overlay is deleted when the VM stops. + Defaults to false. + type: boolean + targetHostPath: + description: |- + TargetHostPath specifies where to place the overlay file on the host. + If empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks). + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + targetPVC: + description: |- + TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored. + If specified, the overlay will be persisted to this PVC instead of node-local storage. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether the volume can + be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + type: object persistentVolumeClaim: description: |- PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. @@ -18724,6 +18922,105 @@ var CRDsValidation map[string]string = map[string]string{ Must be a DNS_LABEL and unique within the vmi. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string + overlay: + description: |- + Overlay represents a qcow2 overlay volume with explicit backing and target sources. + Provides copy-on-write semantics with more control than ephemeral volumes. + properties: + backingFormat: + description: |- + BackingFormat specifies the format of the backing image (raw or qcow2). + Defaults to raw if not specified. + type: string + backingHostPath: + description: BackingHostPath represents a pre-existing + host file or directory used as the backing source. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + backingPVC: + description: BackingPVC is a reference to a PersistentVolumeClaim + used as the backing (read-only) source. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether the volume + can be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + persistent: + description: |- + Persistent indicates whether the overlay should be kept across VM restarts. + If false, the overlay is deleted when the VM stops. + Defaults to false. + type: boolean + targetHostPath: + description: |- + TargetHostPath specifies where to place the overlay file on the host. + If empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks). + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + targetPVC: + description: |- + TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored. + If specified, the overlay will be persisted to this PVC instead of node-local storage. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether the volume + can be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + type: object persistentVolumeClaim: description: |- PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. @@ -23230,6 +23527,105 @@ var CRDsValidation map[string]string = map[string]string{ Must be a DNS_LABEL and unique within the vmi. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string + overlay: + description: |- + Overlay represents a qcow2 overlay volume with explicit backing and target sources. + Provides copy-on-write semantics with more control than ephemeral volumes. + properties: + backingFormat: + description: |- + BackingFormat specifies the format of the backing image (raw or qcow2). + Defaults to raw if not specified. + type: string + backingHostPath: + description: BackingHostPath represents a pre-existing + host file or directory used as the backing source. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + backingPVC: + description: BackingPVC is a reference to a PersistentVolumeClaim + used as the backing (read-only) source. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether + the volume can be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + persistent: + description: |- + Persistent indicates whether the overlay should be kept across VM restarts. + If false, the overlay is deleted when the VM stops. + Defaults to false. + type: boolean + targetHostPath: + description: |- + TargetHostPath specifies where to place the overlay file on the host. + If empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks). + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + targetPVC: + description: |- + TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored. + If specified, the overlay will be persisted to this PVC instead of node-local storage. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether + the volume can be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + type: object persistentVolumeClaim: description: |- PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. @@ -28429,6 +28825,107 @@ var CRDsValidation map[string]string = map[string]string{ Must be a DNS_LABEL and unique within the vmi. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string + overlay: + description: |- + Overlay represents a qcow2 overlay volume with explicit backing and target sources. + Provides copy-on-write semantics with more control than ephemeral volumes. + properties: + backingFormat: + description: |- + BackingFormat specifies the format of the backing image (raw or qcow2). + Defaults to raw if not specified. + type: string + backingHostPath: + description: BackingHostPath represents a + pre-existing host file or directory used + as the backing source. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + backingPVC: + description: BackingPVC is a reference to + a PersistentVolumeClaim used as the backing + (read-only) source. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether + the volume can be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + persistent: + description: |- + Persistent indicates whether the overlay should be kept across VM restarts. + If false, the overlay is deleted when the VM stops. + Defaults to false. + type: boolean + targetHostPath: + description: |- + TargetHostPath specifies where to place the overlay file on the host. + If empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks). + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + targetPVC: + description: |- + TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored. + If specified, the overlay will be persisted to this PVC instead of node-local storage. + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + hotpluggable: + description: Hotpluggable indicates whether + the volume can be hotplugged and hotunplugged. + type: boolean + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + type: object persistentVolumeClaim: description: |- PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. diff --git a/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachine.json b/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachine.json index 2f59ffaee600..66672e62b87b 100644 --- a/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachine.json +++ b/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachine.json @@ -765,6 +765,28 @@ "readOnly": true } }, + "overlay": { + "backingPVC": { + "claimName": "claimNameValue", + "readOnly": true, + "hotpluggable": true + }, + "backingHostPath": { + "path": "pathValue", + "type": "typeValue" + }, + "targetHostPath": { + "path": "pathValue", + "type": "typeValue" + }, + "targetPVC": { + "claimName": "claimNameValue", + "readOnly": true, + "hotpluggable": true + }, + "backingFormat": "backingFormatValue", + "persistent": true + }, "emptyDisk": { "capacity": "0" }, diff --git a/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachine.yaml b/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachine.yaml index d0eace9143ab..7d3c77894418 100644 --- a/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachine.yaml +++ b/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachine.yaml @@ -755,6 +755,23 @@ spec: hotpluggable: true readOnly: true name: nameValue + overlay: + backingFormat: backingFormatValue + backingHostPath: + path: pathValue + type: typeValue + backingPVC: + claimName: claimNameValue + hotpluggable: true + readOnly: true + persistent: true + targetHostPath: + path: pathValue + type: typeValue + targetPVC: + claimName: claimNameValue + hotpluggable: true + readOnly: true persistentVolumeClaim: claimName: claimNameValue hotpluggable: true diff --git a/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachineInstance.json b/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachineInstance.json index 3eeca0f664bd..02876666344c 100644 --- a/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachineInstance.json +++ b/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachineInstance.json @@ -705,6 +705,28 @@ "readOnly": true } }, + "overlay": { + "backingPVC": { + "claimName": "claimNameValue", + "readOnly": true, + "hotpluggable": true + }, + "backingHostPath": { + "path": "pathValue", + "type": "typeValue" + }, + "targetHostPath": { + "path": "pathValue", + "type": "typeValue" + }, + "targetPVC": { + "claimName": "claimNameValue", + "readOnly": true, + "hotpluggable": true + }, + "backingFormat": "backingFormatValue", + "persistent": true + }, "emptyDisk": { "capacity": "0" }, diff --git a/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachineInstance.yaml b/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachineInstance.yaml index 58a6cd41dae1..f214b9ff23bc 100644 --- a/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachineInstance.yaml +++ b/staging/src/kubevirt.io/api/apitesting/testdata/HEAD/kubevirt.io.v1.VirtualMachineInstance.yaml @@ -562,6 +562,23 @@ spec: hotpluggable: true readOnly: true name: nameValue + overlay: + backingFormat: backingFormatValue + backingHostPath: + path: pathValue + type: typeValue + backingPVC: + claimName: claimNameValue + hotpluggable: true + readOnly: true + persistent: true + targetHostPath: + path: pathValue + type: typeValue + targetPVC: + claimName: claimNameValue + hotpluggable: true + readOnly: true persistentVolumeClaim: claimName: claimNameValue hotpluggable: true diff --git a/staging/src/kubevirt.io/api/core/v1/deepcopy_generated.go b/staging/src/kubevirt.io/api/core/v1/deepcopy_generated.go index 1e45d6b30a7b..7e5f093ff110 100644 --- a/staging/src/kubevirt.io/api/core/v1/deepcopy_generated.go +++ b/staging/src/kubevirt.io/api/core/v1/deepcopy_generated.go @@ -3421,6 +3421,42 @@ func (in *NodePlacement) DeepCopy() *NodePlacement { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OverlayVolumeSource) DeepCopyInto(out *OverlayVolumeSource) { + *out = *in + if in.BackingPVC != nil { + in, out := &in.BackingPVC, &out.BackingPVC + *out = new(PersistentVolumeClaimVolumeSource) + **out = **in + } + if in.BackingHostPath != nil { + in, out := &in.BackingHostPath, &out.BackingHostPath + *out = new(corev1.HostPathVolumeSource) + (*in).DeepCopyInto(*out) + } + if in.TargetHostPath != nil { + in, out := &in.TargetHostPath, &out.TargetHostPath + *out = new(corev1.HostPathVolumeSource) + (*in).DeepCopyInto(*out) + } + if in.TargetPVC != nil { + in, out := &in.TargetPVC, &out.TargetPVC + *out = new(PersistentVolumeClaimVolumeSource) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OverlayVolumeSource. +func (in *OverlayVolumeSource) DeepCopy() *OverlayVolumeSource { + if in == nil { + return nil + } + out := new(OverlayVolumeSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PITTimer) DeepCopyInto(out *PITTimer) { *out = *in @@ -6117,6 +6153,11 @@ func (in *VolumeSource) DeepCopyInto(out *VolumeSource) { *out = new(EphemeralVolumeSource) (*in).DeepCopyInto(*out) } + if in.Overlay != nil { + in, out := &in.Overlay, &out.Overlay + *out = new(OverlayVolumeSource) + (*in).DeepCopyInto(*out) + } if in.EmptyDisk != nil { in, out := &in.EmptyDisk, &out.EmptyDisk *out = new(EmptyDiskSource) diff --git a/staging/src/kubevirt.io/api/core/v1/schema.go b/staging/src/kubevirt.io/api/core/v1/schema.go index 454580f5c2da..ffe912d98d23 100644 --- a/staging/src/kubevirt.io/api/core/v1/schema.go +++ b/staging/src/kubevirt.io/api/core/v1/schema.go @@ -814,6 +814,10 @@ type VolumeSource struct { // Ephemeral is a special volume source that "wraps" specified source and provides copy-on-write image on top of it. // +optional Ephemeral *EphemeralVolumeSource `json:"ephemeral,omitempty"` + // Overlay represents a qcow2 overlay volume with explicit backing and target sources. + // Provides copy-on-write semantics with more control than ephemeral volumes. + // +optional + Overlay *OverlayVolumeSource `json:"overlay,omitempty"` // EmptyDisk represents a temporary disk which shares the vmis lifecycle. // More info: https://kubevirt.gitbooks.io/user-guide/disks-and-volumes.html // +optional @@ -893,6 +897,34 @@ type EphemeralVolumeSource struct { PersistentVolumeClaim *v1.PersistentVolumeClaimVolumeSource `json:"persistentVolumeClaim,omitempty"` } +// OverlayVolumeSource represents a qcow2 overlay volume with a backing source. +// The overlay provides copy-on-write semantics on top of the backing source. +type OverlayVolumeSource struct { + // BackingPVC is a reference to a PersistentVolumeClaim used as the backing (read-only) source. + // +optional + BackingPVC *PersistentVolumeClaimVolumeSource `json:"backingPVC,omitempty"` + // BackingHostPath represents a pre-existing host file or directory used as the backing source. + // +optional + BackingHostPath *v1.HostPathVolumeSource `json:"backingHostPath,omitempty"` + // TargetHostPath specifies where to place the overlay file on the host. + // If empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks). + // +optional + TargetHostPath *v1.HostPathVolumeSource `json:"targetHostPath,omitempty"` + // TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored. + // If specified, the overlay will be persisted to this PVC instead of node-local storage. + // +optional + TargetPVC *PersistentVolumeClaimVolumeSource `json:"targetPVC,omitempty"` + // BackingFormat specifies the format of the backing image (raw or qcow2). + // Defaults to raw if not specified. + // +optional + BackingFormat string `json:"backingFormat,omitempty"` + // Persistent indicates whether the overlay should be kept across VM restarts. + // If false, the overlay is deleted when the VM stops. + // Defaults to false. + // +optional + Persistent bool `json:"persistent,omitempty"` +} + // EmptyDisk represents a temporary disk which shares the vmis lifecycle. type EmptyDiskSource struct { // Capacity of the sparse disk. diff --git a/staging/src/kubevirt.io/api/core/v1/schema_swagger_generated.go b/staging/src/kubevirt.io/api/core/v1/schema_swagger_generated.go index 14b91c844670..080482c3affc 100644 --- a/staging/src/kubevirt.io/api/core/v1/schema_swagger_generated.go +++ b/staging/src/kubevirt.io/api/core/v1/schema_swagger_generated.go @@ -442,6 +442,7 @@ func (VolumeSource) SwaggerDoc() map[string]string { "sysprep": "Represents a Sysprep volume source.\n+optional", "containerDisk": "ContainerDisk references a docker image, embedding a qcow or raw disk.\nMore info: https://kubevirt.gitbooks.io/user-guide/registry-disk.html\n+optional", "ephemeral": "Ephemeral is a special volume source that \"wraps\" specified source and provides copy-on-write image on top of it.\n+optional", + "overlay": "Overlay represents a qcow2 overlay volume with explicit backing and target sources.\nProvides copy-on-write semantics with more control than ephemeral volumes.\n+optional", "emptyDisk": "EmptyDisk represents a temporary disk which shares the vmis lifecycle.\nMore info: https://kubevirt.gitbooks.io/user-guide/disks-and-volumes.html\n+optional", "dataVolume": "DataVolume represents the dynamic creation a PVC for this volume as well as\nthe process of populating that PVC with a disk image.\n+optional", "configMap": "ConfigMapSource represents a reference to a ConfigMap in the same namespace.\nMore info: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/\n+optional", @@ -485,6 +486,18 @@ func (EphemeralVolumeSource) SwaggerDoc() map[string]string { } } +func (OverlayVolumeSource) SwaggerDoc() map[string]string { + return map[string]string{ + "": "OverlayVolumeSource represents a qcow2 overlay volume with a backing source.\nThe overlay provides copy-on-write semantics on top of the backing source.", + "backingPVC": "BackingPVC is a reference to a PersistentVolumeClaim used as the backing (read-only) source.\n+optional", + "backingHostPath": "BackingHostPath represents a pre-existing host file or directory used as the backing source.\n+optional", + "targetHostPath": "TargetHostPath specifies where to place the overlay file on the host.\nIf empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks).\n+optional", + "targetPVC": "TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored.\nIf specified, the overlay will be persisted to this PVC instead of node-local storage.\n+optional", + "backingFormat": "BackingFormat specifies the format of the backing image (raw or qcow2).\nDefaults to raw if not specified.\n+optional", + "persistent": "Persistent indicates whether the overlay should be kept across VM restarts.\nIf false, the overlay is deleted when the VM stops.\nDefaults to false.\n+optional", + } +} + func (EmptyDiskSource) SwaggerDoc() map[string]string { return map[string]string{ "": "EmptyDisk represents a temporary disk which shares the vmis lifecycle.", diff --git a/staging/src/kubevirt.io/client-go/api/openapi_generated.go b/staging/src/kubevirt.io/client-go/api/openapi_generated.go index aa36fcb0700e..c92d31d97c01 100644 --- a/staging/src/kubevirt.io/client-go/api/openapi_generated.go +++ b/staging/src/kubevirt.io/client-go/api/openapi_generated.go @@ -460,6 +460,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "kubevirt.io/api/core/v1.NoCloudSSHPublicKeyAccessCredentialPropagation": schema_kubevirtio_api_core_v1_NoCloudSSHPublicKeyAccessCredentialPropagation(ref), "kubevirt.io/api/core/v1.NodeMediatedDeviceTypesConfig": schema_kubevirtio_api_core_v1_NodeMediatedDeviceTypesConfig(ref), "kubevirt.io/api/core/v1.NodePlacement": schema_kubevirtio_api_core_v1_NodePlacement(ref), + "kubevirt.io/api/core/v1.OverlayVolumeSource": schema_kubevirtio_api_core_v1_OverlayVolumeSource(ref), "kubevirt.io/api/core/v1.PITTimer": schema_kubevirtio_api_core_v1_PITTimer(ref), "kubevirt.io/api/core/v1.PauseOptions": schema_kubevirtio_api_core_v1_PauseOptions(ref), "kubevirt.io/api/core/v1.PciHostDevice": schema_kubevirtio_api_core_v1_PciHostDevice(ref), @@ -22662,6 +22663,59 @@ func schema_kubevirtio_api_core_v1_NodePlacement(ref common.ReferenceCallback) c } } +func schema_kubevirtio_api_core_v1_OverlayVolumeSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "OverlayVolumeSource represents a qcow2 overlay volume with a backing source. The overlay provides copy-on-write semantics on top of the backing source.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "backingPVC": { + SchemaProps: spec.SchemaProps{ + Description: "BackingPVC is a reference to a PersistentVolumeClaim used as the backing (read-only) source.", + Ref: ref("kubevirt.io/api/core/v1.PersistentVolumeClaimVolumeSource"), + }, + }, + "backingHostPath": { + SchemaProps: spec.SchemaProps{ + Description: "BackingHostPath represents a pre-existing host file or directory used as the backing source.", + Ref: ref("k8s.io/api/core/v1.HostPathVolumeSource"), + }, + }, + "targetHostPath": { + SchemaProps: spec.SchemaProps{ + Description: "TargetHostPath specifies where to place the overlay file on the host. If empty, defaults to node-local storage (/var/run/kubevirt-private/overlay-disks).", + Ref: ref("k8s.io/api/core/v1.HostPathVolumeSource"), + }, + }, + "targetPVC": { + SchemaProps: spec.SchemaProps{ + Description: "TargetPVC specifies a PersistentVolumeClaim where the overlay should be stored. If specified, the overlay will be persisted to this PVC instead of node-local storage.", + Ref: ref("kubevirt.io/api/core/v1.PersistentVolumeClaimVolumeSource"), + }, + }, + "backingFormat": { + SchemaProps: spec.SchemaProps{ + Description: "BackingFormat specifies the format of the backing image (raw or qcow2). Defaults to raw if not specified.", + Type: []string{"string"}, + Format: "", + }, + }, + "persistent": { + SchemaProps: spec.SchemaProps{ + Description: "Persistent indicates whether the overlay should be kept across VM restarts. If false, the overlay is deleted when the VM stops. Defaults to false.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/api/core/v1.HostPathVolumeSource", "kubevirt.io/api/core/v1.PersistentVolumeClaimVolumeSource"}, + } +} + func schema_kubevirtio_api_core_v1_PITTimer(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -27329,6 +27383,12 @@ func schema_kubevirtio_api_core_v1_Volume(ref common.ReferenceCallback) common.O Ref: ref("kubevirt.io/api/core/v1.EphemeralVolumeSource"), }, }, + "overlay": { + SchemaProps: spec.SchemaProps{ + Description: "Overlay represents a qcow2 overlay volume with explicit backing and target sources. Provides copy-on-write semantics with more control than ephemeral volumes.", + Ref: ref("kubevirt.io/api/core/v1.OverlayVolumeSource"), + }, + }, "emptyDisk": { SchemaProps: spec.SchemaProps{ Description: "EmptyDisk represents a temporary disk which shares the vmis lifecycle. More info: https://kubevirt.gitbooks.io/user-guide/disks-and-volumes.html", @@ -27382,7 +27442,7 @@ func schema_kubevirtio_api_core_v1_Volume(ref common.ReferenceCallback) common.O }, }, Dependencies: []string{ - "kubevirt.io/api/core/v1.CloudInitConfigDriveSource", "kubevirt.io/api/core/v1.CloudInitNoCloudSource", "kubevirt.io/api/core/v1.ConfigMapVolumeSource", "kubevirt.io/api/core/v1.ContainerDiskSource", "kubevirt.io/api/core/v1.DataVolumeSource", "kubevirt.io/api/core/v1.DownwardAPIVolumeSource", "kubevirt.io/api/core/v1.DownwardMetricsVolumeSource", "kubevirt.io/api/core/v1.EmptyDiskSource", "kubevirt.io/api/core/v1.EphemeralVolumeSource", "kubevirt.io/api/core/v1.HostDisk", "kubevirt.io/api/core/v1.MemoryDumpVolumeSource", "kubevirt.io/api/core/v1.PersistentVolumeClaimVolumeSource", "kubevirt.io/api/core/v1.SecretVolumeSource", "kubevirt.io/api/core/v1.ServiceAccountVolumeSource", "kubevirt.io/api/core/v1.SysprepSource"}, + "kubevirt.io/api/core/v1.CloudInitConfigDriveSource", "kubevirt.io/api/core/v1.CloudInitNoCloudSource", "kubevirt.io/api/core/v1.ConfigMapVolumeSource", "kubevirt.io/api/core/v1.ContainerDiskSource", "kubevirt.io/api/core/v1.DataVolumeSource", "kubevirt.io/api/core/v1.DownwardAPIVolumeSource", "kubevirt.io/api/core/v1.DownwardMetricsVolumeSource", "kubevirt.io/api/core/v1.EmptyDiskSource", "kubevirt.io/api/core/v1.EphemeralVolumeSource", "kubevirt.io/api/core/v1.HostDisk", "kubevirt.io/api/core/v1.MemoryDumpVolumeSource", "kubevirt.io/api/core/v1.OverlayVolumeSource", "kubevirt.io/api/core/v1.PersistentVolumeClaimVolumeSource", "kubevirt.io/api/core/v1.SecretVolumeSource", "kubevirt.io/api/core/v1.ServiceAccountVolumeSource", "kubevirt.io/api/core/v1.SysprepSource"}, } } @@ -27504,6 +27564,12 @@ func schema_kubevirtio_api_core_v1_VolumeSource(ref common.ReferenceCallback) co Ref: ref("kubevirt.io/api/core/v1.EphemeralVolumeSource"), }, }, + "overlay": { + SchemaProps: spec.SchemaProps{ + Description: "Overlay represents a qcow2 overlay volume with explicit backing and target sources. Provides copy-on-write semantics with more control than ephemeral volumes.", + Ref: ref("kubevirt.io/api/core/v1.OverlayVolumeSource"), + }, + }, "emptyDisk": { SchemaProps: spec.SchemaProps{ Description: "EmptyDisk represents a temporary disk which shares the vmis lifecycle. More info: https://kubevirt.gitbooks.io/user-guide/disks-and-volumes.html", @@ -27556,7 +27622,7 @@ func schema_kubevirtio_api_core_v1_VolumeSource(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "kubevirt.io/api/core/v1.CloudInitConfigDriveSource", "kubevirt.io/api/core/v1.CloudInitNoCloudSource", "kubevirt.io/api/core/v1.ConfigMapVolumeSource", "kubevirt.io/api/core/v1.ContainerDiskSource", "kubevirt.io/api/core/v1.DataVolumeSource", "kubevirt.io/api/core/v1.DownwardAPIVolumeSource", "kubevirt.io/api/core/v1.DownwardMetricsVolumeSource", "kubevirt.io/api/core/v1.EmptyDiskSource", "kubevirt.io/api/core/v1.EphemeralVolumeSource", "kubevirt.io/api/core/v1.HostDisk", "kubevirt.io/api/core/v1.MemoryDumpVolumeSource", "kubevirt.io/api/core/v1.PersistentVolumeClaimVolumeSource", "kubevirt.io/api/core/v1.SecretVolumeSource", "kubevirt.io/api/core/v1.ServiceAccountVolumeSource", "kubevirt.io/api/core/v1.SysprepSource"}, + "kubevirt.io/api/core/v1.CloudInitConfigDriveSource", "kubevirt.io/api/core/v1.CloudInitNoCloudSource", "kubevirt.io/api/core/v1.ConfigMapVolumeSource", "kubevirt.io/api/core/v1.ContainerDiskSource", "kubevirt.io/api/core/v1.DataVolumeSource", "kubevirt.io/api/core/v1.DownwardAPIVolumeSource", "kubevirt.io/api/core/v1.DownwardMetricsVolumeSource", "kubevirt.io/api/core/v1.EmptyDiskSource", "kubevirt.io/api/core/v1.EphemeralVolumeSource", "kubevirt.io/api/core/v1.HostDisk", "kubevirt.io/api/core/v1.MemoryDumpVolumeSource", "kubevirt.io/api/core/v1.OverlayVolumeSource", "kubevirt.io/api/core/v1.PersistentVolumeClaimVolumeSource", "kubevirt.io/api/core/v1.SecretVolumeSource", "kubevirt.io/api/core/v1.ServiceAccountVolumeSource", "kubevirt.io/api/core/v1.SysprepSource"}, } }