diff --git a/.github/workflows/dev_module_build-and-registration.yml b/.github/workflows/dev_module_build-and-registration.yml index 45a3496a8f..9aa79b895f 100644 --- a/.github/workflows/dev_module_build-and-registration.yml +++ b/.github/workflows/dev_module_build-and-registration.yml @@ -24,21 +24,12 @@ env: RELEASE_CHANNEL: ${{ github.event.inputs.channel }} MODULES_MODULE_TAG: ${{ github.event.inputs.tag }} SOURCE_REPO: "${{secrets.SOURCE_REPO}}" + GO_VERSION: "1.22.7" + MODULE_EDITION: "EE" on: workflow_dispatch: inputs: - channel: - description: "Select release channel" - type: choice - default: alpha - options: - - "alpha" - - "beta" - - "early-access" - - "stable" - - "rock-solid" - tag: description: "The module's tag, which must include the -dev postfix. For example: v1.21.1-dev" type: string @@ -49,9 +40,17 @@ on: default: true description: "Set to true if build is required" +defaults: + run: + shell: bash + +concurrency: + group: "${{ github.workflow }}-${{ github.event.number || github.ref }}" + cancel-in-progress: true + jobs: deploy-dev: - runs-on: ubuntu-latest + runs-on: [self-hosted, regular, selectel] name: Deploy dev steps: - name: PRINT VARS @@ -62,6 +61,7 @@ jobs: echo MODULES_MODULE_NAME=$MODULES_MODULE_NAME echo RELEASE_CHANNEL=$RELEASE_CHANNEL echo MODULES_MODULE_TAG=$MODULES_MODULE_TAG + echo MODULE_EDITION=$MODULE_EDITION shell: bash - name: Validation for tag run: | @@ -69,10 +69,9 @@ jobs: shell: bash - uses: actions/checkout@v4 - - - name: Remove unwanted software - if: ${{ github.event.inputs.enableBuild == 'true' }} - uses: ./.github/actions/remove-unwanted-software + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} - uses: deckhouse/modules-actions/setup@v1 - if: ${{ github.event.inputs.enableBuild == 'true' }} diff --git a/.github/workflows/dev_module_build.yml b/.github/workflows/dev_module_build.yml index 552fccd6ff..cfa06158df 100644 --- a/.github/workflows/dev_module_build.yml +++ b/.github/workflows/dev_module_build.yml @@ -28,6 +28,9 @@ on: workflow_dispatch: inputs: pr_number: + description: | + Pull request number, like 563, or leave empty and choose a branch + For branches main, pre-alpha, release-*, tag will be generated as branch name required: false type: number pull_request: @@ -36,6 +39,7 @@ on: branches: - main - pre-alpha + - release-* defaults: run: @@ -77,12 +81,14 @@ jobs: run: | if [[ "${{ github.ref_name }}" == 'pre-alpha' || "${{ github.ref_name }}" == 'main' ]]; then MODULES_MODULE_TAG="${{ github.ref_name }}" + elif [[ "${{ github.ref_name }}" =~ ^release-[0-9]+\.[0-9]+ ]]; then + MODULES_MODULE_TAG="${{ github.ref_name }}" elif [[ -n "${{ github.event.pull_request.number }}" ]]; then MODULES_MODULE_TAG="pr${{ github.event.pull_request.number }}" elif [[ -n "${{ github.event.inputs.pr_number }}" ]]; then MODULES_MODULE_TAG="pr${{ github.event.inputs.pr_number }}" else - echo "::error title=Module image tag is required::Can't detect module tag from workflow context. Dev build uses branch name as tag for pre-alpha and main branches, and PR number for builds from pull requests. Check workflow for correctness." + echo "::error title=Module image tag is required::Can't detect module tag from workflow context. Dev build uses branch name as tag for pre-alpha, main and release branches, and PR number for builds from pull requests. Check workflow for correctness." exit 1 fi diff --git a/CHANGELOG/CHANGELOG-v0.16.0.yml b/CHANGELOG/CHANGELOG-v0.16.0.yml new file mode 100644 index 0000000000..a7dbcc0e1a --- /dev/null +++ b/CHANGELOG/CHANGELOG-v0.16.0.yml @@ -0,0 +1,76 @@ +api: + features: + - summary: create a network policy for the importer to avoid restrictions during import to dvcr + pull_request: https://github.com/deckhouse/virtualization/pull/675 + - summary: provide dvcr connection error from DataVolume + pull_request: https://github.com/deckhouse/virtualization/pull/666 + fixes: + - summary: reduce max len of block device names to avoid errors during vm creation + pull_request: https://github.com/deckhouse/virtualization/pull/737 +ci: + fixes: + - summary: fix ci for release branch build + pull_request: https://github.com/deckhouse/virtualization/pull/729 + - summary: fix import path for cdi images (cloner,controller) + pull_request: https://github.com/deckhouse/virtualization/pull/725 + - summary: fix runner choose + pull_request: https://github.com/deckhouse/virtualization/pull/697 + - summary: cleanup namespaces after e2e tests + pull_request: https://github.com/deckhouse/virtualization/pull/682 +core: + fixes: + - summary: manage pods network priority during a migration using the cilium label + pull_request: https://github.com/deckhouse/virtualization/pull/642 +docs: + fixes: + - summary: fix alert in virtualMachineCIDRs parameter + pull_request: https://github.com/deckhouse/virtualization/pull/677 +images: + fixes: + - summary: images can be successfully created from images on immediate storage class + pull_request: https://github.com/deckhouse/virtualization/pull/712 +kubevirt: + fixes: + - summary: >- + Improve unmounting process by cleaning up mount point files and verifying actual mount + states + pull_request: https://github.com/deckhouse/virtualization/pull/683 +vd: + fixes: + - summary: add WaitingForFirstConsumer phase for virtual disks created from snapshots + pull_request: https://github.com/deckhouse/virtualization/pull/704 +vi: + features: + - summary: add the ability to create a VirtualImage from a VirtualDiskSnapshot + pull_request: https://github.com/deckhouse/virtualization/pull/617 +vm: + features: + - summary: >- + move error about exceeding the allowed number of block devices from + DiskAttachmentCapacityAvailable into BlockDevicesReady condition + pull_request: https://github.com/deckhouse/virtualization/pull/633 + fixes: + - summary: >- + disk serials are now generated using the MD5 hash of the disk uid instead of the disk name + itself; this prevents errors caused by recent QEMU changes enforcing a strict 36-character + limit on serial numbers + pull_request: https://github.com/deckhouse/virtualization/pull/710 + - summary: truncate prefixes + pull_request: https://github.com/deckhouse/virtualization/pull/705 + - summary: >- + Disk serials are now generated using the MD5 hash of the disk name instead of the disk name + itself. This prevents errors caused by recent QEMU changes enforcing a strict 36-character + limit on serial numbers. + pull_request: https://github.com/deckhouse/virtualization/pull/690 +vmbda: + fixes: + - summary: add serial for hotplug vi, cvi + pull_request: https://github.com/deckhouse/virtualization/pull/735 +vmclass: + features: + - summary: add events about available nodes and sizing policies changed + pull_request: https://github.com/deckhouse/virtualization/pull/606 +vmip: + features: + - summary: add new events + pull_request: https://github.com/deckhouse/virtualization/pull/645 diff --git a/CHANGELOG/CHANGELOG-v0.16.1.yml b/CHANGELOG/CHANGELOG-v0.16.1.yml new file mode 100644 index 0000000000..ebf167379c --- /dev/null +++ b/CHANGELOG/CHANGELOG-v0.16.1.yml @@ -0,0 +1,32 @@ +core: + fixes: + - summary: >- + add validatingadmissionpolicies and validatingadmissionpolicybindings to rbac for + virt-operator (required for kubernetes >=v1.30) + pull_request: https://github.com/deckhouse/virtualization/pull/749 +vdsnapshot: + fixes: + - summary: set the volume snapshot name to status + pull_request: https://github.com/deckhouse/virtualization/pull/778 +vi: + fixes: + - summary: add watcher for virtv2 virtual machines + pull_request: https://github.com/deckhouse/virtualization/pull/775 + - summary: >- + fix the possible hanging on the Terminating state for the virtual image that was attached to + a virtual machine + pull_request: https://github.com/deckhouse/virtualization/pull/755 + - summary: >- + fixed hang in Pending when creating an image from a snapshot, improved messages in + DatasourceReady condition + pull_request: https://github.com/deckhouse/virtualization/pull/721 +vm: + fixes: + - summary: >- + move the vm to a pending phase instead of starting it if it has invalid specs during + start/restart to prevent the use of outdated specifications + pull_request: https://github.com/deckhouse/virtualization/pull/678 +vmop: + fixes: + - summary: fixed resource hang in InProgress during Evict/Migrate operations + pull_request: https://github.com/deckhouse/virtualization/pull/758 diff --git a/CHANGELOG/CHANGELOG-v0.16.md b/CHANGELOG/CHANGELOG-v0.16.md new file mode 100644 index 0000000000..a2548e0374 --- /dev/null +++ b/CHANGELOG/CHANGELOG-v0.16.md @@ -0,0 +1,30 @@ +# Changelog v0.16 + +## Features + + + - **[api]** create a network policy for the importer to avoid restrictions during import to dvcr [#675](https://github.com/deckhouse/virtualization/pull/675) + - **[vi]** add the ability to create a VirtualImage from a VirtualDiskSnapshot [#617](https://github.com/deckhouse/virtualization/pull/617) + - **[vm]** move error about exceeding the allowed number of block devices from DiskAttachmentCapacityAvailable into BlockDevicesReady condition [#633](https://github.com/deckhouse/virtualization/pull/633) + - **[vmclass]** add events about available nodes and sizing policies changed [#606](https://github.com/deckhouse/virtualization/pull/606) + - **[vmip]** add new events [#645](https://github.com/deckhouse/virtualization/pull/645) + +## Fixes + + + - **[api]** reduce max len of block device names to avoid errors during vm creation [#737](https://github.com/deckhouse/virtualization/pull/737) + - **[core]** add validatingadmissionpolicies and validatingadmissionpolicybindings to rbac for virt-operator (required for kubernetes >=v1.30) [#749](https://github.com/deckhouse/virtualization/pull/749) + - **[core]** manage pods network priority during a migration using the cilium label [#642](https://github.com/deckhouse/virtualization/pull/642) + - **[images]** images can be successfully created from images on immediate storage class [#712](https://github.com/deckhouse/virtualization/pull/712) + - **[vd]** add WaitingForFirstConsumer phase for virtual disks created from snapshots [#704](https://github.com/deckhouse/virtualization/pull/704) + - **[vi]** fix the possible hanging on the Terminating state for the virtual image that was attached to a virtual machine [#755](https://github.com/deckhouse/virtualization/pull/755) + - **[vi]** fixed hang in Pending when creating an image from a snapshot, improved messages in DatasourceReady condition [#721](https://github.com/deckhouse/virtualization/pull/721) + - **[vm]** move the vm to a pending phase instead of starting it if it has invalid specs during start/restart to prevent the use of outdated specifications [#678](https://github.com/deckhouse/virtualization/pull/678) + - **[vm]** disk serials are now generated using the MD5 hash of the disk uid instead of the disk name itself; this prevents errors caused by recent QEMU changes enforcing a strict 36-character limit on serial numbers [#710](https://github.com/deckhouse/virtualization/pull/710) + - **[vmop]** fixed resource hang in InProgress during Evict/Migrate operations [#758](https://github.com/deckhouse/virtualization/pull/758) + +## Chore + + + - **[core]** fix build firmware (edk2) using with ovmf 4MB instead of 2MB [#707](https://github.com/deckhouse/virtualization/pull/707) + diff --git a/api/core/v1alpha2/events.go b/api/core/v1alpha2/events.go index eb1aa37a56..ed1b6d8b0d 100644 --- a/api/core/v1alpha2/events.go +++ b/api/core/v1alpha2/events.go @@ -61,6 +61,9 @@ const ( // ReasonVMOPStarted is event reason that the operation is started ReasonVMOPStarted = "VirtualMachineOperationStarted" + // ReasonVMOPInProgress is event reason that the operation is in progress + ReasonVMOPInProgress = "VirtualMachineOperationInProgress" + // ReasonVDStorageClassWasDeleted is event reason that VDStorageClass was deleted. ReasonVDStorageClassWasDeleted = "VirtualDiskStorageClassWasDeleted" // ReasonVDStorageClassNotFound is event reason that VDStorageClass not found. diff --git a/api/core/v1alpha2/vdscondition/condition.go b/api/core/v1alpha2/vdscondition/condition.go index 148c357e28..26506bd3f9 100644 --- a/api/core/v1alpha2/vdscondition/condition.go +++ b/api/core/v1alpha2/vdscondition/condition.go @@ -59,7 +59,7 @@ const ( WaitingForTheVirtualDisk VirtualDiskSnapshotReadyReason = "WaitingForTheVirtualDisk" // PotentiallyInconsistent signifies that the snapshotting process cannot begin because creating a snapshot of virtual disk attached to the running virtual machine might result in an inconsistent snapshot. PotentiallyInconsistent VirtualDiskSnapshotReadyReason = "PotentiallyInconsistent" - // VolumeSnapshotLost signifies that the underling `VolumeSnapshot` is lost: cannot use the virtual disk snapshot as a data source. + // VolumeSnapshotLost signifies that the underlying `VolumeSnapshot` is lost: cannot use the virtual disk snapshot as a data source. VolumeSnapshotLost VirtualDiskSnapshotReadyReason = "Lost" // FileSystemFreezing signifies that the `VirtualDiskSnapshot` resource is in the process of freezing the filesystem of the virtual machine associated with the source virtual disk. FileSystemFreezing VirtualDiskSnapshotReadyReason = "FileSystemFreezing" diff --git a/api/core/v1alpha2/vicondition/condition.go b/api/core/v1alpha2/vicondition/condition.go index dc12517fdb..2a69d104ec 100644 --- a/api/core/v1alpha2/vicondition/condition.go +++ b/api/core/v1alpha2/vicondition/condition.go @@ -64,6 +64,8 @@ const ( ClusterImageNotReady DatasourceReadyReason = "ClusterImageNotReady" // VirtualDiskNotReady indicates that the `VirtualDisk` datasource is not ready, which prevents the import process from starting. VirtualDiskNotReady DatasourceReadyReason = "VirtualDiskNotReady" + // VirtualDiskSnapshotNotReady indicates that the `VirtualDiskSnapshot` datasource is not ready, which prevents the import process from starting. + VirtualDiskSnapshotNotReady DatasourceReadyReason = "VirtualDiskSnapshotNotReady" // VirtualDiskInUseInRunningVirtualMachine indicates that the `VirtualDisk` attached to running `VirtualMachine` VirtualDiskInUseInRunningVirtualMachine DatasourceReadyReason = "VirtualDiskInUseInRunningVirtualMachine" diff --git a/api/core/v1alpha2/vmscondition/condition.go b/api/core/v1alpha2/vmscondition/condition.go index 4d27d6b135..e953414975 100644 --- a/api/core/v1alpha2/vmscondition/condition.go +++ b/api/core/v1alpha2/vmscondition/condition.go @@ -47,7 +47,7 @@ const ( BlockDevicesNotReady VirtualMachineSnapshotReadyReason = "BlockDevicesNotReady" // PotentiallyInconsistent signifies that the snapshotting process cannot begin because creating a snapshot of the running virtual machine might result in an inconsistent snapshot. PotentiallyInconsistent VirtualMachineSnapshotReadyReason = "PotentiallyInconsistent" - // VirtualDiskSnapshotLost signifies that the underling `VirtualDiskSnapshot` is lost: cannot restore the virtual machine using this snapshot. + // VirtualDiskSnapshotLost signifies that the underlying `VirtualDiskSnapshot` is lost: cannot restore the virtual machine using this snapshot. VirtualDiskSnapshotLost VirtualMachineSnapshotReadyReason = "VirtualDiskSnapshotLost" // FileSystemFreezing signifies that the `VirtualMachineSnapshot` resource is in the process of freezing the filesystem of the virtual machine. FileSystemFreezing VirtualMachineSnapshotReadyReason = "FileSystemFreezing" diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 7166d8d280..a64e65e467 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -3,39 +3,133 @@ title: "Installation" weight: 15 --- -## DVP Requirements +> **Warning.** The platform components must be deployed on physical servers (bare-metal servers). +> +> Installation on virtual machines is allowed for demonstration purposes only, but nested virtualization must be enabled. If the platform is deployed on virtual machines, technical support will not be provided. -### Resource requirements: +## Platform scalability -The following minimum resources are recommended for infrastructure nodes, depending on their role in the cluster: +The platform supports the following configuration: -- Master node-4 CPUs, 8 GB of RAM, 60 GB of disk space on a fast disk (400+ IOPS); -- Worker node-the requirements are similar to those for the master node, but largely depend on the nature of the load running on the node (nodes). +- Maximum number of nodes: 1000. +- Maximum number of virtual machines: 50000. -> If you plan to use the virtualization module in a production environment, it is recommended to deploy it on physical servers. Deploying the module on virtual machines is also possible, but in this case you need to enable nested virtualization. +The platform has no other restrictions and is compatible with any hardware that is supported by [operating systems](#supported-os-for-platform-nodes) on which it can be installed. -### Requirements for platform nodes: +## Hardware Requirements -- Linux-based OS: - - CentOS 7, 8, 9 - - Debian 10, 11, 12 - - Rocky Linux 8, 9 - - Ubuntu 18.04, 20.04, 22.04, 24.04 -- Linux kernel version >= 5.7 -- CPU with x86_64 c architecture with support for Intel-VT (vmx) or AMD-V (svm) instructions +1. A dedicated **machine for installation**. -## Installation + This machine will run the Deckhouse installer. For example, it can be an administrator's laptop or any other computer that is not intended to be added to the cluster. Requirements for this machine: -1. Deploy the Deckhouse Kubernetes Platform cluster by [instruction](https://deckhouse.io/products/kubernetes-platform/gs/). + - OS: Windows 10+, macOS 10.15+, Linux (Ubuntu 18.04+, Fedora 35+); + - Installed Docker Engine or Docker Desktop (instructions for [Ubuntu](https://docs.docker.com/engine/install/ubuntu/), [macOS](https://docs.docker.com/desktop/mac/install/), [Windows](https://docs.docker.com/desktop/windows/install/)); + - HTTPS access to the container image registry at `registry.deckhouse.io`; + - SSH key-based access to the node that will serve as the **master node** of the future cluster; + - SSH key-based access to the node that will serve as the **worker node** of the future cluster (if the cluster will consist of more than one master node). + +1. **Server for the master node** + + There can be multiple servers running the cluster’s control plane components, but only one server is required at installation time. The others can be added later via node management mechanisms. + + Requirements for a physical bare-metal server: + + - Resources: + - CPU: + - x86_64 architecture; + - Support for Intel-VT (VMX) or AMD-V (SVM) instructions; + - At least 4 cores. + - RAM: At least 8 GB. + - Disk space: + - At least 60 GB; + - High-speed disk (400+ IOPS). + - OS [from the list of supported ones](#supported-os-for-platform-nodes): + - Linux kernel version `5.7` or newer. + - **Unique hostname** across all servers in the future cluster; + - Network access: + - HTTPS access to the container image registry at `registry.deckhouse.io`; + - Access to the package repositories of the chosen OS; + - SSH key-based access from the **installation machine** (see p.1); + - Network access from the **installation machine** (see p.1) on port `22322/TCP`. + - Required software: + - The `cloud-utils` and `cloud-init` packages must be installed (package names may vary depending on the chosen OS). + > **Warning.** The container runtime will be installed automatically, so do not pre-install any `containerd` or `docker` packages. + +1. **Servers for worker nodes** + + These nodes will run virtual machines, so the servers must have enough resources to handle the planned number of VMs. Additional disks may be required if you deploy a software-defined storage solution. + + Requirements for a physical bare-metal server: + + - Resources: + - CPU: + - x86_64 architecture; + - Support for Intel-VT (VMX) or AMD-V (SVM) instructions; + - At least 4 cores; + - RAM: At least 8 GB; + - Disk space: + - At least 60 GB; + - High-speed disk (400+ IOPS); + - Additional disks for software-defined storage; + - OS [from the list of supported ones](#supported-os-for-platform-nodes); + - Linux kernel version `5.7` or newer; + - **Unique hostname** across all servers in the future cluster; + - Network access: + - HTTPS access to the container image registry at `registry.deckhouse.io`; + - Access to the package repositories of the chosen OS; + - SSH key-based access from the **installation machine** (see p.1); + - Required software: + - The `cloud-utils` and `cloud-init` packages must be installed (package names may vary depending on the chosen OS). + > **Important.** The container runtime will be installed automatically, so do not pre-install any `containerd` or `docker` packages. + +1. **Storage hardware** + + Depending on the chosen storage solution, additional resources may be required. For details, refer to the section [Storage Management](/products/virtualization-platform/documentation/admin/platform-management/storage/sds/lvm-local.html). + +## Supported OS for platform nodes + +| Linux distribution | Supported versions | +| --------------------------- | ------------------------------- | +| CentOS | 7, 8, 9 | +| Debian | 10, 11, 12 | +| Ubuntu | 20.04, 22.04, 24.04 | -2. Enable the necessary modules. +## Supported guest operating systems - To store virtual machine data (virtual disks and images), you must enable one or more of the following modules according to the installation instructions: +The virtualization platform supports operating systems running on `x86` and `x86_64` architectures as guest operating systems. For correct operation in paravirtualization mode, `VirtIO` drivers must be installed to ensure efficient interaction between the virtual machine and the hypervisor. + +Successful startup of the operating system is determined by the following criteria: + + * correct installation and booting of the OS; + * uninterrupted operation of key components such as networking and storage; + * no crashes or errors during operation. + +For Linux family operating systems it is recommended to use guest OS images with `cloud-init` support, which allows initializing virtual machines after their creation. + +For Windows operating systems, the platform supports initialization using the built-in sysprep utility. + +## Supported virtual machine configurations + +Maximum number of cores supported: `254` +Maximum amount of RAM: `1024 GB` + +## Supported Storage Systems + +Virtual machines use `PersistentVolume` resources. To manage these resources and allocate disk space within the cluster, one or more supported storage systems must be installed: + +| Storage System | Disk Location | +|---------------------------------------------|----------------------------| +| LVM (Logical Volume Manager) | Local | +| DRBD (Distributed Replicated Block Device) | Replicas on cluster nodes | +| Ceph Cluster | External storage | +| NFS (Network File System) | External storage | +| TATLIN.UNIFIED (Yadro) | External storage | + +## Installation + +1. Deploy the Deckhouse Kubernetes Platform cluster by [instruction](https://deckhouse.io/products/kubernetes-platform/gs/). - - [SDS-Replicated-volume](https://deckhouse.io/modules/sds-replicated-volume/stable/) - - [SDS-Local-volume](https://deckhouse.io/modules/sds-local-volume/stable/) - - [CSI-nfs](https://deckhouse.io/modules/csi-nfs/stable/) - - [CEPH-CSI](/documentation/v1/modules/031-ceph-csi/) +2. To store virtual machine data (virtual disks and images), you must enable one or more supported [storage](#supported-storage-systems). 3. [Set](https://kubernetes.io/docs/tasks/administer-cluster/change-default-storage-class/) default `StorageClass`. 4. Turn on the [console](https://deckhouse.io/modules/console/stable/) module, which will allow you to manage virtualization components through via UI (This feature is available only to users of the EE edition). diff --git a/docs/INSTALL_RU.md b/docs/INSTALL_RU.md index bdca0191d7..16f6eacedf 100644 --- a/docs/INSTALL_RU.md +++ b/docs/INSTALL_RU.md @@ -3,45 +3,140 @@ title: "Установка" weight: 15 --- -## Требования к DVP +> **Внимание.** Компоненты платформы необходимо развертывать на физических серверах (bare-metal). +> +> Установка на виртуальные машины допустима только в демонстрационных целях, но при этом должна быть включена вложенная виртуализация (nested virtualization). Если платформа развернута на виртуальных машинах, техническая поддержка не предоставляется. + +## Возможности масштабирования платформы + +Платформа поддерживает следующую конфигурацию: + +- Максимальное количество узлов: 1000. +- Максимальное количество виртуальных машин: 50000. -### Требования к ресурсам: +Платформа не имеет дополнительных ограничений и совместима с любым оборудованием, которое поддерживается [операционными системами](#поддерживаемые-ос-для-узлов-платформы), на которые она может быть установлена. -Рекомендуются следующие минимальные ресурсы для инфраструктурных узлов в зависимости от их роли в кластере: +## Требования к аппаратному обеспечению -- Мастер-узел — 4 CPU, 8 ГБ RAM, 60 ГБ дискового пространства на быстром диске (400+ IOPS); -- Worker-узел — требования аналогичны требованиям к master-узлу, но во многом зависят от характера запускаемой на узле (узлах) нагрузки. +1. Отдельная **машина для установки**. -> Если вы планируете использовать модуль виртуализации в промышленной среде, рекомендуется развернуть его на физических серверах. Развертывание модуля на виртуальных машинах также возможно, но в этом случае необходимо включить вложенную виртуализацию. + Здесь будет запускаться установщик Deckhouse. Это может быть ноутбук администратора или любой другой компьютер, который не планируется добавлять в кластер. Требования к этой машине: -### Требования к узлам платформы: + - ОС: Windows 10+, macOS 10.15+, Linux (Ubuntu 18.04+, Fedora 35+); + - Установленный Docker Engine или Docker Desktop (инструкции [для Ubuntu](https://docs.docker.com/engine/install/ubuntu/), [macOS](https://docs.docker.com/desktop/mac/install/), [Windows](https://docs.docker.com/desktop/windows/install/)); + - HTTPS-доступ к хранилищу образов контейнеров `registry.deckhouse.ru`; + - SSH-доступ по ключу к узлу, который будет **master-узлом** будущего кластера; + - SSH-доступ по ключу к узлу, который будет **worker-узлом** будущего кластера (если кластер будет состоять не из одного master-узла). -- ОС на базе Linux: - - РЕД ОС 7.3, 8.0 - - РОСА Сервер 7.9, 12.4, 12.5.1 - - ALT Linux p10, 10.0, 10.1, 10.2, 11 - - Astra Linux Special Edition 1.7.2, 1.7.3, 1.7.4, 1.7.5 - - CentOS 7, 8, 9 - - Debian 10, 11, 12 - - Rocky Linux 8, 9 - - Ubuntu 18.04, 20.04, 22.04, 24.04 -- Версия ядра Linux >= 5.7 -- ЦП с архитектурой x86_64 c с поддержкой инструкций Intel-VT (vmx) или AMD-V (svm) +1. **Сервер для master-узла.** -## Порядок установки + Серверов для запуска управляющих компонентов кластера может быть несколько. Для установки достаточно одного сервера, а остальные нужно будет добавить через механизмы управления узлами. -1. Разверните кластер Deckhouse Kubernetes Platform [по инструкции](https://deckhouse.ru/gs/). + Требования к физическому bare metal-серверу: + + - Ресурсы: + - Процессор: + - Архитектура x86_64; + - Поддержка инструкций Intel-VT (VMX) или AMD-V (SVM); + - не менее 4 ядер; + - ОЗУ не менее 8 ГБ; + - Дисковое пространство: + - не менее 60 ГБ; + - быстрый диск (400+ IOPS); + - ОС [из списка поддерживаемых](#поддерживаемые-ос-для-узлов-платформы): + - ядро Linux версии `5.7` или новее; + - **Уникальный hostname** среди всех серверов будущего кластера; + - Сетевые доступы: + - HTTPS-доступ к хранилищу образов контейнеров `registry.deckhouse.ru`; + - доступ к репозиториям пакетов используемой ОС; + - SSH-доступ от **машины для установки** (см. п.1) по ключу; + - сетевой доступ от **машины для установки** (см. п.1) по порту `22322/TCP`; + - Требуемое ПО: + - пакеты `cloud-utils` и `cloud-init` должны быть установлены. + > **Важно.** Container runtime будет установлен автоматически, поэтому пакеты `containerd` и/или `docker` не должны быть установлены. + +1. **Серверы для worker-узлов.** + + Это узлы, где будут запускаться виртуальные машины, поэтому ресурсов на этих серверах должно хватать для запуска планируемого количества виртуальных машин. При использовании программно-определяемого хранилища могут потребоваться дополнительные диски. + + Требования к физическому bare metal-серверу: + + - Ресурсы: + - Процессор: + - Архитектура x86_64; + - Поддержка инструкций Intel-VT (VMX) или AMD-V (SVM); + - не менее 4 ядер. + - ОЗУ не менее 8 ГБ. + - Дисковое пространство: + - не менее 60 ГБ; + - быстрый диск (400+ IOPS); + - дополнительные диски для программно-определяемого хранилища. + - ОС [из списка поддерживаемых](#поддерживаемые-ос-для-узлов-платформы): + - ядро Linux версии `5.7` или новее. + - **Уникальный hostname** среди всех серверов будущего кластера; + - Сетевые доступы: + - HTTPS-доступ к хранилищу образов контейнеров `registry.deckhouse.ru`; + - доступ к репозиториям пакетов используемой ОС; + - SSH-доступ от **машины для установки** (см. п.1) по ключу. + - Требуемое ПО: + - пакеты `cloud-utils` и `cloud-init` должны быть установлены (названия могут отличаться в зависимости от выбранной ОС). + > **Важно.** Container runtime будет установлен автоматически, поэтому пакеты `containerd` и/или `docker` не должны быть установлены. + +1. **Оборудование для хранилища.** + + В зависимости от выбранного хранилища могут потребоваться дополнительные ресурсы. Подробности смотрите в разделе [Управление хранилищами](/products/virtualization-platform/documentation/admin/platform-management/storage/sds/lvm-local.html). + +## Поддерживаемые ОС для узлов платформы + +| Дистрибутив Linux | Поддерживаемые версии | +| --------------------------- | ------------------------------- | +| РЕД ОС | 7.3, 8.0 | +| РОСА Сервер | 7.9, 12.4, 12.5.1 | +| ALT Linux | p10, 10.0, 10.1, 10.2, 11 | +| Astra Linux Special Edition | 1.7.2, 1.7.3, 1.7.4, 1.7.5, 1.8 | +| CentOS | 7, 8, 9 | +| Debian | 10, 11, 12 | +| Ubuntu | 18.04, 20.04, 22.04, 24.04 | -2. Включите необходимые модули. +## Поддерживаемые гостевые ОС - Для хранения данных виртуальных машин (виртуальные диски и образы) необходимо включить один или несколько из следующих модулей согласно инструкции по их установке: +Платформа виртуализации поддерживает операционные системы, работающие на архитектурах `x86` и `x86_64`, в качестве гостевых ОС. Для корректной работы в режиме паравиртуализации необходимо установить драйверы `VirtIO`, обеспечивающие эффективное взаимодействие между виртуальной машиной и гипервизором. - - [SDS-Replicated-volume](https://deckhouse.ru/modules/sds-replicated-volume/stable/) - - [SDS-Local-volume](https://deckhouse.ru/modules/sds-local-volume/stable/) - - [CSI-nfs](https://deckhouse.ru/modules/csi-nfs/stable/) - - [CEPH-CSI](/documentation/v1/modules/031-ceph-csi/) +Успешный запуск операционной системы определяется следующими критериями: + + * корректная установка и загрузка ОС; + * бесперебойная работа основных компонентов, таких как сеть и хранилище; + * отсутствие сбоев или ошибок в процессе работы. + +Для операционных систем семейства Linux рекомендуется использовать образы гостевых ОС с поддержкой `cloud-init`, что позволяет выполнять инициализацию виртуальных машин после их создания. + +Для операционных систем семейства Windows платформа поддерживает инициализацию с помощью встроенной утилиты sysprep. + +## Поддерживаемые конфигурации виртуальных машин + +Максимальное число поддерживаемых ядер: `254` +Максимальный объем оперативной памяти: `1024 Гб` + +## Поддерживаемые хранилища + +Виртуальные машины используют ресурсы `PersistentVolume`. Для управления этими ресурсами и выделения дискового пространства в кластере должно быть установлено одно или несколько поддерживаемых хранилищ: + +| Хранилище | Расположение дисков | +|--------------------------------------------|----------------------------| +| LVM (Logical Volume Manager) | Локальное | +| DRBD (Distributed Replicated Block Device) | Реплики на узлах кластера | +| Ceph-кластер | Внешнее хранилище | +| NFS (Network File System) | Внешнее хранилище | +| TATLIN.UNIFIED (Yadro) | Внешнее хранилище | + +## Порядок установки + +1. Разверните кластер Deckhouse Kubernetes Platform [по инструкции](https://deckhouse.ru/gs/). + +2. Для хранения данных виртуальных машин (виртуальные диски и образы) необходимо включить один или несколько поддерживаемых [хранилищ](#поддерживаемые-хранилища). 3. [Установите](https://kubernetes.io/docs/tasks/administer-cluster/change-default-storage-class/) `StorageClass` по умолчанию. + 4. Включите модуль [console](https://deckhouse.ru/modules/console/stable/), который позволит управлять компонентами виртуализации через графический интерфейс (данная возможность доступна только пользователям EE-редакции). 5. Включите модуль `virtualization`: @@ -65,7 +160,7 @@ kind: ModuleConfig metadata: name: virtualization spec: - enabled: true # включить модуль + enabled: true # Включить модуль. settings: dvcr: storage: diff --git a/docs/USER_GUIDE_RU.md b/docs/USER_GUIDE_RU.md index 637f6ccafc..801d2962df 100644 --- a/docs/USER_GUIDE_RU.md +++ b/docs/USER_GUIDE_RU.md @@ -1057,7 +1057,7 @@ spec: disktype: ssd ``` -![](images/placement-node-affinity.ru.png) +![](images/placement-nodeselector.ru.png) В этом примере виртуальная машина будет размещена только на узлах, которые имеют метку `disktype` со значением `ssd`. diff --git a/images/virtualization-artifact/pkg/common/annotations/annotations.go b/images/virtualization-artifact/pkg/common/annotations/annotations.go index 39afbb9b51..6b984a7737 100644 --- a/images/virtualization-artifact/pkg/common/annotations/annotations.go +++ b/images/virtualization-artifact/pkg/common/annotations/annotations.go @@ -70,6 +70,12 @@ const ( AnnOsType = AnnAPIGroupV + "/os-type" + // AnnVmStartRequested is an annotation on KVVM that represents a request to start a virtual machine. + AnnVmStartRequested = AnnAPIGroupV + "/vm-start-requested" + + // AnnVmRestartRequested is an annotation on KVVM that represents a request to restart a virtual machine. + AnnVmRestartRequested = AnnAPIGroupV + "/vm-restart-requested" + // LabelsPrefix is a prefix for virtualization-controller labels. LabelsPrefix = "virtualization.deckhouse.io" diff --git a/images/virtualization-artifact/pkg/common/blockdevice/block_device.go b/images/virtualization-artifact/pkg/common/blockdevice/block_device.go new file mode 100644 index 0000000000..52ae035e7a --- /dev/null +++ b/images/virtualization-artifact/pkg/common/blockdevice/block_device.go @@ -0,0 +1,32 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package blockdevice + +// MaxDiskNameLen determines the max len of vd. +// Disk and volume name in kubevirt can be a valid container name (len 63) since disk name can become a container name which will fail to schedule if invalid. +// We add prefix "vd-" for the vd name, so max len reduced to 60. +const MaxDiskNameLen = 60 + +// MaxVirtualImageNameLen determines the max len of vi. +// Disk and volume name in kubevirt can be a valid container name (len 63) since disk name can become a container name which will fail to schedule if invalid. +// We and kubevirt add prefixes "vi-", "volume" and suffix "-init", so max len reduced to 49. +const MaxVirtualImageNameLen = 49 + +// MaxClusterVirtualImageNameLen determines the max len of cvi. +// Disk and volume name in kubevirt can be a valid container name (len 63) since disk name can become a container name which will fail to schedule if invalid. +// We and kubevirt add prefixes "cvi-", "volume" and suffix "-init", so max len reduced to 48. +const MaxClusterVirtualImageNameLen = 48 diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/runner.go b/images/virtualization-artifact/pkg/common/blockdevice/runner.go similarity index 73% rename from images/virtualization-artifact/pkg/controller/vd/internal/source/step/runner.go rename to images/virtualization-artifact/pkg/common/blockdevice/runner.go index 22c7110c87..409f331c00 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/step/runner.go +++ b/images/virtualization-artifact/pkg/common/blockdevice/runner.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package step +package blockdevice import ( "context" @@ -29,17 +29,17 @@ type Resource interface { *virtv2.VirtualDisk | *virtv2.VirtualImage } -type Taker[R Resource] interface { +type StepTaker[R Resource] interface { Take(ctx context.Context, obj R) (*reconcile.Result, error) } -type Takers[R Resource] []Taker[R] +type StepTakers[R Resource] []StepTaker[R] -func NewTakers[R Resource](takers ...Taker[R]) Takers[R] { +func NewStepTakers[R Resource](takers ...StepTaker[R]) StepTakers[R] { return takers } -func (steps Takers[R]) Run(ctx context.Context, r R) (reconcile.Result, error) { +func (steps StepTakers[R]) Run(ctx context.Context, r R) (reconcile.Result, error) { for _, s := range steps { res, err := s.Take(ctx, r) if err != nil { @@ -51,5 +51,5 @@ func (steps Takers[R]) Run(ctx context.Context, r R) (reconcile.Result, error) { } } - return reconcile.Result{}, errors.New("todo unexpected") + return reconcile.Result{}, errors.New("none of the steps returned a final result, please report a bug") } diff --git a/images/virtualization-artifact/pkg/common/kvvm/kvvm.go b/images/virtualization-artifact/pkg/common/kvvm/kvvm.go index fd0edca0da..9d01c52601 100644 --- a/images/virtualization-artifact/pkg/common/kvvm/kvvm.go +++ b/images/virtualization-artifact/pkg/common/kvvm/kvvm.go @@ -19,6 +19,7 @@ package kvvm import ( "context" "fmt" + "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" @@ -26,7 +27,9 @@ import ( virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/common/patch" ) // PatchRunStrategy returns JSON merge patch to set 'runStrategy' field to the desired value @@ -98,3 +101,77 @@ func DeletePodByKVVMI(ctx context.Context, cli client.Client, kvvmi *virtv1.Virt } return object.DeleteObject(ctx, cli, pod, opts) } + +func AddRestartAnnotation(ctx context.Context, cl client.Client, kvvm *virtv1.VirtualMachine) error { + if kvvm.Annotations[annotations.AnnVmStartRequested] == "" { + jp := patch.NewJsonPatch( + patch.NewJsonPatchOperation(patch.PatchReplaceOp, fmt.Sprintf("/metadata/annotations/%s", escapeJSONPointer(annotations.AnnVmRestartRequested)), "true"), + ) + bytes, err := jp.Bytes() + if err != nil { + return err + } + err = cl.Patch(ctx, kvvm, client.RawPatch(types.JSONPatchType, bytes)) + if err != nil { + return err + } + } + + return nil +} + +func AddStartAnnotation(ctx context.Context, cl client.Client, kvvm *virtv1.VirtualMachine) error { + if kvvm.Annotations[annotations.AnnVmStartRequested] == "" { + jp := patch.NewJsonPatch( + patch.NewJsonPatchOperation(patch.PatchReplaceOp, fmt.Sprintf("/metadata/annotations/%s", escapeJSONPointer(annotations.AnnVmStartRequested)), "true"), + ) + bytes, err := jp.Bytes() + if err != nil { + return err + } + err = cl.Patch(ctx, kvvm, client.RawPatch(types.JSONPatchType, bytes)) + if err != nil { + return err + } + } + + return nil +} + +func escapeJSONPointer(path string) string { + return strings.ReplaceAll(path, "/", "~1") +} + +func RemoveStartAnnotation(ctx context.Context, cl client.Client, kvvm *virtv1.VirtualMachine) error { + if kvvm.Annotations[annotations.AnnVmStartRequested] != "" { + jp := patch.NewJsonPatch( + patch.NewJsonPatchOperation(patch.PatchReplaceOp, fmt.Sprintf("/metadata/annotations/%s", escapeJSONPointer(annotations.AnnVmStartRequested)), ""), + ) + bytes, err := jp.Bytes() + if err != nil { + return err + } + err = cl.Patch(ctx, kvvm, client.RawPatch(types.JSONPatchType, bytes)) + if err != nil { + return err + } + } + return nil +} + +func RemoveRestartAnnotation(ctx context.Context, cl client.Client, kvvm *virtv1.VirtualMachine) error { + if kvvm.Annotations[annotations.AnnVmRestartRequested] != "" { + jp := patch.NewJsonPatch( + patch.NewJsonPatchOperation(patch.PatchReplaceOp, fmt.Sprintf("/metadata/annotations/%s", escapeJSONPointer(annotations.AnnVmRestartRequested)), ""), + ) + bytes, err := jp.Bytes() + if err != nil { + return err + } + err = cl.Patch(ctx, kvvm, client.RawPatch(types.JSONPatchType, bytes)) + if err != nil { + return err + } + } + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_webhook.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_webhook.go index d187ecb03d..089ecbf333 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/cvi_webhook.go +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_webhook.go @@ -18,7 +18,6 @@ package cvi import ( "context" - "errors" "fmt" "strings" @@ -27,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/common/blockdevice" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" @@ -48,8 +48,12 @@ func (v *Validator) ValidateCreate(_ context.Context, obj runtime.Object) (admis return nil, fmt.Errorf("expected a new ClusterVirtualImage but got a %T", obj) } - if strings.Contains(cvi.ObjectMeta.Name, ".") { - return nil, errors.New("ClusterVirtualImage name is invalid: '.' is forbidden, allowed name symbols are [0-9a-zA-Z-]") + if strings.Contains(cvi.Name, ".") { + return nil, fmt.Errorf("the ClusterVirtualImage name %q is invalid: '.' is forbidden, allowed name symbols are [0-9a-zA-Z-]", cvi.Name) + } + + if len(cvi.Name) > blockdevice.MaxClusterVirtualImageNameLen { + return nil, fmt.Errorf("the ClusterVirtualImage name %q is too long: it must be no more than %d characters", cvi.Name, blockdevice.MaxClusterVirtualImageNameLen) } return nil, nil @@ -79,8 +83,12 @@ func (v *Validator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Obj return nil, fmt.Errorf("ClusterVirtualImage is in a Ready state: configuration changes are not available") } - if strings.Contains(newCVI.ObjectMeta.Name, ".") { - warnings = append(warnings, "ClusterVirtualImage name is invalid as it contains now forbidden symbol '.', allowed symbols for name are [0-9a-zA-Z-]. Create another image with valid name to avoid problems with future updates.") + if strings.Contains(newCVI.Name, ".") { + warnings = append(warnings, fmt.Sprintf("the ClusterVirtualImage name %q is invalid as it contains now forbidden symbol '.', allowed symbols for name are [0-9a-zA-Z-]. Create another image with valid name to avoid problems with future updates.", newCVI.Name)) + } + + if len(newCVI.Name) > blockdevice.MaxClusterVirtualImageNameLen { + warnings = append(warnings, fmt.Sprintf("the ClusterVirtualImage name %q is too long: it must be no more than %d characters", newCVI.Name, blockdevice.MaxClusterVirtualImageNameLen)) } return warnings, nil diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref_vdsnapshot.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref_vdsnapshot.go index 6938f50c7f..ff303fd5e3 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref_vdsnapshot.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref_vdsnapshot.go @@ -78,7 +78,7 @@ func NewObjectRefVirtualDiskSnapshot( func (ds ObjectRefVirtualDiskSnapshot) Sync(ctx context.Context, cvi *virtv2.ClusterVirtualImage, vdSnapshotRef *virtv2.VirtualDiskSnapshot, cb *conditions.ConditionBuilder) (reconcile.Result, error) { log, ctx := logger.GetDataSourceContext(ctx, "objectref") - supgen := supplements.NewGenerator(annotations.VIShortName, cvi.Name, vdSnapshotRef.Namespace, cvi.UID) + supgen := supplements.NewGenerator(annotations.CVIShortName, cvi.Name, vdSnapshotRef.Namespace, cvi.UID) pod, err := ds.importerService.GetPod(ctx, supgen) if err != nil { return reconcile.Result{}, err @@ -129,7 +129,7 @@ func (ds ObjectRefVirtualDiskSnapshot) Sync(ctx context.Context, cvi *virtv2.Clu "The ObjectRef DataSource import has started", ) - namespacedName := supplements.NewGenerator(annotations.VIShortName, cvi.Name, cvi.Spec.DataSource.ObjectRef.Namespace, cvi.UID).PersistentVolumeClaim() + pvcKey := supplements.NewGenerator(annotations.CVIShortName, cvi.Name, cvi.Spec.DataSource.ObjectRef.Namespace, cvi.UID).PersistentVolumeClaim() storageClassName := vs.Annotations["storageClass"] volumeMode := vs.Annotations["volumeMode"] @@ -166,8 +166,8 @@ func (ds ObjectRefVirtualDiskSnapshot) Sync(ctx context.Context, cvi *virtv2.Clu pvc = &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ - Name: namespacedName.Name, - Namespace: namespacedName.Namespace, + Name: pvcKey.Name, + Namespace: pvcKey.Namespace, OwnerReferences: []metav1.OwnerReference{ service.MakeOwnerReference(cvi), }, @@ -306,7 +306,7 @@ func (ds ObjectRefVirtualDiskSnapshot) Sync(ctx context.Context, cvi *virtv2.Clu } func (ds ObjectRefVirtualDiskSnapshot) CleanUpSupplements(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (reconcile.Result, error) { - supgen := supplements.NewGenerator(annotations.VIShortName, cvi.Name, cvi.Spec.DataSource.ObjectRef.Namespace, cvi.UID) + supgen := supplements.NewGenerator(annotations.CVIShortName, cvi.Name, cvi.Spec.DataSource.ObjectRef.Namespace, cvi.UID) importerRequeue, err := ds.importerService.CleanUpSupplements(ctx, supgen) if err != nil { @@ -327,7 +327,7 @@ func (ds ObjectRefVirtualDiskSnapshot) CleanUpSupplements(ctx context.Context, c } func (ds ObjectRefVirtualDiskSnapshot) CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { - supgen := supplements.NewGenerator(annotations.VIShortName, cvi.Name, cvi.Spec.DataSource.ObjectRef.Namespace, cvi.UID) + supgen := supplements.NewGenerator(annotations.CVIShortName, cvi.Name, cvi.Spec.DataSource.ObjectRef.Namespace, cvi.UID) importerRequeue, err := ds.importerService.CleanUp(ctx, supgen) if err != nil { diff --git a/images/virtualization-artifact/pkg/controller/importer/importer_pod.go b/images/virtualization-artifact/pkg/controller/importer/importer_pod.go index a3e34b70b8..7225f5c916 100644 --- a/images/virtualization-artifact/pkg/controller/importer/importer_pod.go +++ b/images/virtualization-artifact/pkg/controller/importer/importer_pod.go @@ -91,6 +91,7 @@ type PodSettings struct { PriorityClassName string PVCName string NodePlacement *provisioner.NodePlacement + Finalizer string } // CreatePod creates and returns a pointer to a pod which is created based on the passed-in endpoint, secret @@ -126,6 +127,9 @@ func (imp *Importer) makeImporterPodSpec() (*corev1.Pod, error) { Annotations: map[string]string{ annotations.AnnCreatedBy: "yes", }, + Finalizers: []string{ + imp.PodSettings.Finalizer, + }, OwnerReferences: []metav1.OwnerReference{ imp.PodSettings.OwnerReference, }, diff --git a/images/virtualization-artifact/pkg/controller/indexer/indexer.go b/images/virtualization-artifact/pkg/controller/indexer/indexer.go index 9153312382..c75b122b97 100644 --- a/images/virtualization-artifact/pkg/controller/indexer/indexer.go +++ b/images/virtualization-artifact/pkg/controller/indexer/indexer.go @@ -37,10 +37,11 @@ const ( IndexFieldVMIPLeaseByVMIP = "spec.virtualMachineIPAddressRef.Name" - IndexFieldVDByVDSnapshot = "spec.DataSource.ObjectRef.Name,.Kind=VirtualDiskSnapshot" + IndexFieldVDByVDSnapshot = "vd,spec.DataSource.ObjectRef.Name,.Kind=VirtualDiskSnapshot" + IndexFieldVIByVDSnapshot = "vi,spec.DataSource.ObjectRef.Name,.Kind=VirtualDiskSnapshot" - IndexFieldVDByStorageClass = "VD.spec.PersistentVolumeClaim.StorageClass" - IndexFieldVIByStorageClass = "VI.spec.PersistentVolumeClaim.StorageClass" + IndexFieldVDByStorageClass = "vd.spec.PersistentVolumeClaim.StorageClass" + IndexFieldVIByStorageClass = "vi.spec.PersistentVolumeClaim.StorageClass" IndexFieldVMSnapshotByVM = "spec.virtualMachineName" IndexFieldVMSnapshotByVDSnapshot = "status.virtualDiskSnapshotNames" @@ -62,12 +63,13 @@ func IndexALL(ctx context.Context, mgr manager.Manager) error { IndexVMByVI, IndexVMByCVI, IndexVMIPLeaseByVMIP, - IndexVDByVDSnapshot, IndexVMSnapshotByVM, IndexVMSnapshotByVDSnapshot, IndexVMRestoreByVMSnapshot, IndexVMIPByVM, + IndexVDByVDSnapshot, IndexVDByStorageClass, + IndexVIByVDSnapshot, IndexVIByStorageClass, IndexVMIPByAddress, IndexVMBDAByVM, diff --git a/images/virtualization-artifact/pkg/controller/indexer/vi_indexer.go b/images/virtualization-artifact/pkg/controller/indexer/vi_indexer.go index 38fec2fd08..0415d33538 100644 --- a/images/virtualization-artifact/pkg/controller/indexer/vi_indexer.go +++ b/images/virtualization-artifact/pkg/controller/indexer/vi_indexer.go @@ -25,6 +25,25 @@ import ( virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) +func IndexVIByVDSnapshot(ctx context.Context, mgr manager.Manager) error { + return mgr.GetFieldIndexer().IndexField(ctx, &virtv2.VirtualImage{}, IndexFieldVIByVDSnapshot, func(object client.Object) []string { + vi, ok := object.(*virtv2.VirtualImage) + if !ok || vi == nil { + return nil + } + + if vi.Spec.DataSource.Type != virtv2.DataSourceTypeObjectRef { + return nil + } + + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot { + return nil + } + + return []string{vi.Spec.DataSource.ObjectRef.Name} + }) +} + func IndexVIByStorageClass(ctx context.Context, mgr manager.Manager) error { return mgr.GetFieldIndexer().IndexField(ctx, &virtv2.VirtualImage{}, IndexFieldVIByStorageClass, func(object client.Object) []string { vi, ok := object.(*virtv2.VirtualImage) diff --git a/images/virtualization-artifact/pkg/controller/service/attachment_service.go b/images/virtualization-artifact/pkg/controller/service/attachment_service.go index 092828b4d3..c35cd8d3b7 100644 --- a/images/virtualization-artifact/pkg/controller/service/attachment_service.go +++ b/images/virtualization-artifact/pkg/controller/service/attachment_service.go @@ -292,7 +292,7 @@ func NewAttachmentDiskFromVirtualDisk(vd *virtv2.VirtualDisk) *AttachmentDisk { func NewAttachmentDiskFromVirtualImage(vi *virtv2.VirtualImage) *AttachmentDisk { serial := "" if !vi.Status.CDROM { - kvbuilder.GenerateSerialFromObject(vi) + serial = kvbuilder.GenerateSerialFromObject(vi) } ad := AttachmentDisk{ Kind: virtv2.ImageDevice, @@ -315,7 +315,7 @@ func NewAttachmentDiskFromVirtualImage(vi *virtv2.VirtualImage) *AttachmentDisk func NewAttachmentDiskFromClusterVirtualImage(cvi *virtv2.ClusterVirtualImage) *AttachmentDisk { serial := "" if !cvi.Status.CDROM { - kvbuilder.GenerateSerialFromObject(cvi) + serial = kvbuilder.GenerateSerialFromObject(cvi) } return &AttachmentDisk{ Kind: virtv2.ClusterImageDevice, diff --git a/images/virtualization-artifact/pkg/controller/service/bounder_service.go b/images/virtualization-artifact/pkg/controller/service/bounder_service.go index be40fc4b27..cd1341fa15 100644 --- a/images/virtualization-artifact/pkg/controller/service/bounder_service.go +++ b/images/virtualization-artifact/pkg/controller/service/bounder_service.go @@ -64,8 +64,8 @@ func NewBounderPodService( } } -func (s BounderPodService) Start(ctx context.Context, ownerRef *metav1.OwnerReference, sup *supplements.Generator, pvc *corev1.PersistentVolumeClaim, opts ...Option) error { - podSettings := s.GetPodSettings(ownerRef, sup, pvc) +func (s BounderPodService) Start(ctx context.Context, ownerRef *metav1.OwnerReference, sup *supplements.Generator, opts ...Option) error { + podSettings := s.GetPodSettings(ownerRef, sup) for _, opt := range opts { switch v := opt.(type) { @@ -172,17 +172,17 @@ func (s BounderPodService) GetPod(ctx context.Context, sup *supplements.Generato return pod, nil } -func (s BounderPodService) GetPodSettings(ownerRef *metav1.OwnerReference, sup *supplements.Generator, pvc *corev1.PersistentVolumeClaim) *bounder.PodSettings { +func (s BounderPodService) GetPodSettings(ownerRef *metav1.OwnerReference, sup *supplements.Generator) *bounder.PodSettings { bounderPod := sup.BounderPod() return &bounder.PodSettings{ Name: bounderPod.Name, - Namespace: pvc.Namespace, + Namespace: bounderPod.Namespace, Image: s.image, PullPolicy: s.pullPolicy, OwnerReference: *ownerRef, ControllerName: s.controllerName, InstallerLabels: map[string]string{}, ResourceRequirements: &s.requirements, - PVCName: pvc.Name, + PVCName: sup.PersistentVolumeClaim().Name, } } diff --git a/images/virtualization-artifact/pkg/controller/service/importer_service.go b/images/virtualization-artifact/pkg/controller/service/importer_service.go index 4ed68a71f1..adc75bb073 100644 --- a/images/virtualization-artifact/pkg/controller/service/importer_service.go +++ b/images/virtualization-artifact/pkg/controller/service/importer_service.go @@ -117,6 +117,7 @@ func (s ImporterService) Start( func (s ImporterService) StartWithPodSetting(ctx context.Context, settings *importer.Settings, sup *supplements.Generator, caBundle *datasource.CABundle, podSettings *importer.PodSettings) error { settings.Verbose = s.verbose + podSettings.Finalizer = s.protection.finalizer pod, err := importer.NewImporter(podSettings, settings).CreatePod(ctx, s.client) if err != nil && !k8serrors.IsAlreadyExists(err) { @@ -290,6 +291,7 @@ func (s ImporterService) getPodSettings(ownerRef *metav1.OwnerReference, sup *su ControllerName: s.controllerName, InstallerLabels: map[string]string{}, ResourceRequirements: &s.requirements, + Finalizer: s.protection.GetFinalizer(), } } diff --git a/images/virtualization-artifact/pkg/controller/service/protection_service.go b/images/virtualization-artifact/pkg/controller/service/protection_service.go index 91b6393698..77d96c0321 100644 --- a/images/virtualization-artifact/pkg/controller/service/protection_service.go +++ b/images/virtualization-artifact/pkg/controller/service/protection_service.go @@ -119,6 +119,10 @@ func (s ProtectionService) RemoveProtection(ctx context.Context, objs ...client. return nil } +func (s ProtectionService) GetFinalizer() string { + return s.finalizer +} + func MakeOwnerReference(owner client.Object) metav1.OwnerReference { return metav1.OwnerReference{ APIVersion: owner.GetObjectKind().GroupVersionKind().GroupVersion().String(), diff --git a/images/virtualization-artifact/pkg/controller/service/vm_operation.go b/images/virtualization-artifact/pkg/controller/service/vm_operation.go index f4faa4deee..73e33362cd 100644 --- a/images/virtualization-artifact/pkg/controller/service/vm_operation.go +++ b/images/virtualization-artifact/pkg/controller/service/vm_operation.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/deckhouse/virtualization-controller/pkg/common" + kvvmutil "github.com/deckhouse/virtualization-controller/pkg/common/kvvm" "github.com/deckhouse/virtualization-controller/pkg/common/object" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/powerstate" @@ -76,7 +77,7 @@ func (s VMOperationService) DoStart(ctx context.Context, vmNamespace, vmName str if err != nil { return fmt.Errorf("get kvvm %q: %w", vmName, err) } - return powerstate.StartVM(ctx, s.client, kvvm) + return kvvmutil.AddStartAnnotation(ctx, s.client, kvvm) } func (s VMOperationService) DoStop(ctx context.Context, vmNamespace, vmName string, force bool) error { @@ -92,11 +93,7 @@ func (s VMOperationService) DoRestart(ctx context.Context, vmNamespace, vmName s if err != nil { return fmt.Errorf("get kvvm %q: %w", vmName, err) } - kvvmi, err := s.getKVVMI(ctx, vmNamespace, vmName) - if err != nil { - return fmt.Errorf("get kvvmi %q: %w", vmName, err) - } - return powerstate.RestartVM(ctx, s.client, kvvm, kvvmi, force) + return kvvmutil.AddRestartAnnotation(ctx, s.client, kvvm) } func (s VMOperationService) DoEvict(ctx context.Context, vmNamespace, vmName string) error { diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go index 8aa6b0dacc..c51d3c0035 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/blockdevice" "github.com/deckhouse/virtualization-controller/pkg/common/object" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" @@ -73,7 +74,7 @@ func (ds ObjectRefVirtualDiskSnapshot) Sync(ctx context.Context, vd *virtv2.Virt return reconcile.Result{}, err } - return step.NewTakers[*virtv2.VirtualDisk]( + return blockdevice.NewStepTakers[*virtv2.VirtualDisk]( step.NewReadyStep(ds.diskService, pvc, cb), step.NewTerminatingStep(pvc), step.NewWaitForPVCStep(pvc, ds.client, cb), diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot_test.go index c62a77a0ba..e18ed65bc0 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot_test.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot_test.go @@ -133,11 +133,6 @@ var _ = Describe("ObjectRef VirtualDiskSnapshot", func() { }, }, }, - Status: virtv2.VirtualDiskStatus{ - Target: virtv2.DiskTarget{ - PersistentVolumeClaim: pvc.Name, - }, - }, } }) @@ -222,6 +217,7 @@ var _ = Describe("ObjectRef VirtualDiskSnapshot", func() { Context("VirtualDisk is lost", func() { It("is lost when PVC is not found", func() { + vd.Status.Target.PersistentVolumeClaim = pvc.Name client := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() syncer := NewObjectRefVirtualDiskSnapshot(recorder, svc, client) @@ -232,11 +228,11 @@ var _ = Describe("ObjectRef VirtualDiskSnapshot", func() { ExpectCondition(vd, metav1.ConditionFalse, vdcondition.Lost, true) Expect(vd.Status.Phase).To(Equal(virtv2.DiskLost)) - Expect(vd.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) }) It("is lost when PVC is lost as well", func() { pvc.Status.Phase = corev1.ClaimLost + vd.Status.Target.PersistentVolumeClaim = pvc.Name client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() syncer := NewObjectRefVirtualDiskSnapshot(recorder, svc, client) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/validator/name_validator.go b/images/virtualization-artifact/pkg/controller/vd/internal/validator/name_validator.go index 26c05a4fda..44278b4eaa 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/validator/name_validator.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/validator/name_validator.go @@ -18,11 +18,12 @@ package validator import ( "context" - "errors" + "fmt" "strings" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "github.com/deckhouse/virtualization-controller/pkg/common/blockdevice" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -33,19 +34,27 @@ func NewNameValidator() *NameValidator { } func (v *NameValidator) ValidateCreate(_ context.Context, vd *virtv2.VirtualDisk) (admission.Warnings, error) { - if strings.Contains(vd.ObjectMeta.Name, ".") { - return nil, errors.New("VirtualDisk name is invalid: '.' is forbidden, allowed name symbols are [0-9a-zA-Z-]") + if strings.Contains(vd.Name, ".") { + return nil, fmt.Errorf("the VirtualDisk name %q is invalid: '.' is forbidden, allowed name symbols are [0-9a-zA-Z-]", vd.Name) + } + + if len(vd.Name) > blockdevice.MaxDiskNameLen { + return nil, fmt.Errorf("the VirtualDisk name %q is too long: it must be no more than %d characters", vd.Name, blockdevice.MaxDiskNameLen) } return nil, nil } func (v *NameValidator) ValidateUpdate(_ context.Context, _, newVD *virtv2.VirtualDisk) (admission.Warnings, error) { - if strings.Contains(newVD.ObjectMeta.Name, ".") { - var warnings admission.Warnings - warnings = append(warnings, "VirtualDisk name is invalid as it contains now forbidden symbol '.', allowed symbols for name are [0-9a-zA-Z-]. Create another disk with valid name to avoid problems with future updates.") - return warnings, nil + var warnings admission.Warnings + + if strings.Contains(newVD.Name, ".") { + warnings = append(warnings, fmt.Sprintf("the VirtualDisk name %q is invalid as it contains now forbidden symbol '.', allowed symbols for name are [0-9a-zA-Z-]. Create another disk with valid name to avoid problems with future updates.", newVD.Name)) } - return nil, nil + if len(newVD.Name) > blockdevice.MaxDiskNameLen { + warnings = append(warnings, fmt.Sprintf("the VirtualDisk name %q is too long: it must be no more than %d characters", newVD.Name, blockdevice.MaxDiskNameLen)) + } + + return warnings, nil } diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go index 5ee40020be..58183fe14a 100644 --- a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go @@ -100,10 +100,12 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vdSnapshot *virtv2.Virtual } vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseReady + vdSnapshot.Status.VolumeSnapshotName = vs.Name cb. Status(metav1.ConditionTrue). Reason(vdscondition.VirtualDiskSnapshotReady). Message("") + return reconcile.Result{}, nil } @@ -252,6 +254,7 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vdSnapshot *virtv2.Virtual log.Debug("Waiting for the volume snapshot to be ready to use") vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseInProgress + vdSnapshot.Status.VolumeSnapshotName = vs.Name cb. Status(metav1.ConditionFalse). Reason(vdscondition.Snapshotting). @@ -285,6 +288,7 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vdSnapshot *virtv2.Virtual } vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseReady + vdSnapshot.Status.VolumeSnapshotName = vs.Name cb. Status(metav1.ConditionTrue). Reason(vdscondition.VirtualDiskSnapshotReady). diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/datasource_ready.go b/images/virtualization-artifact/pkg/controller/vi/internal/datasource_ready.go index cc33352756..7c300de66c 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/datasource_ready.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/datasource_ready.go @@ -89,6 +89,12 @@ func (h DatasourceReadyHandler) Handle(ctx context.Context, vi *virtv2.VirtualIm Reason(vicondition.VirtualDiskNotReady). Message(service.CapitalizeFirstLetter(err.Error() + ".")) return reconcile.Result{}, nil + case errors.As(err, &source.VirtualDiskSnapshotNotReadyError{}): + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.VirtualDiskSnapshotNotReady). + Message(service.CapitalizeFirstLetter(err.Error() + ".")) + return reconcile.Result{}, nil case errors.As(err, &source.VirtualDiskNotAllowedForUseError{}): cb. Status(metav1.ConditionFalse). diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go index be9c2eeaca..79a6f053bd 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go @@ -21,39 +21,28 @@ import ( corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - "github.com/deckhouse/virtualization-controller/pkg/controller/bounder" "github.com/deckhouse/virtualization-controller/pkg/controller/importer" "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source/step" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) -//go:generate moq -rm -out mock.go . Importer Uploader Stat Handler +//go:generate moq -rm -out mock.go . Importer Uploader Stat Bounder Handler type Importer interface { - Start(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error - StartWithPodSetting(ctx context.Context, settings *importer.Settings, sup *supplements.Generator, caBundle *datasource.CABundle, podSettings *importer.PodSettings) error - CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) - CleanUpSupplements(ctx context.Context, sup *supplements.Generator) (bool, error) - GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) - Protect(ctx context.Context, pod *corev1.Pod) error - Unprotect(ctx context.Context, pod *corev1.Pod) error - GetPodSettingsWithPVC(ownerRef *metav1.OwnerReference, sup *supplements.Generator, pvcName, pvcNamespace string) *importer.PodSettings -} - -type Bounder interface { - Start(ctx context.Context, ownerRef *metav1.OwnerReference, sup *supplements.Generator, pvc *corev1.PersistentVolumeClaim, opts ...service.Option) error + step.CreatePodStepImporter + step.ReadyContainerRegistryStepImporter CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) CleanUpSupplements(ctx context.Context, sup *supplements.Generator) (bool, error) GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) Protect(ctx context.Context, pod *corev1.Pod) error Unprotect(ctx context.Context, pod *corev1.Pod) error - GetPodSettings(ownerRef *metav1.OwnerReference, sup *supplements.Generator, pvc *corev1.PersistentVolumeClaim) *bounder.PodSettings + Start(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error } type Uploader interface { @@ -70,13 +59,16 @@ type Uploader interface { } type Stat interface { - GetFormat(pod *corev1.Pod) string - GetCDROM(pod *corev1.Pod) bool - GetSize(pod *corev1.Pod) virtv2.ImageStatusSize - GetDVCRImageName(pod *corev1.Pod) string - GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) *virtv2.StatusSpeed - GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string - IsUploaderReady(pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) bool + step.CreatePodStepStat + step.WaitForPodStepStat + step.ReadyContainerRegistryStepStat IsUploadStarted(ownerUID types.UID, pod *corev1.Pod) bool - CheckPod(pod *corev1.Pod) error + IsUploaderReady(pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) bool + GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) *virtv2.StatusSpeed +} + +type Bounder interface { + step.CreateBounderPodStepBounder + CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) + CleanUpSupplements(ctx context.Context, sup *supplements.Generator) (bool, error) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/mock.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/mock.go index 591e614c80..a8ce34b70d 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/mock.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/mock.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + // Code generated by moq; DO NOT EDIT. // github.com/matryer/moq @@ -38,7 +54,7 @@ var _ Importer = &ImporterMock{} // GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { // panic("mock out the GetPod method") // }, -// GetPodSettingsWithPVCFunc: func(ownerRef *metav1.OwnerReference, sup *supplements.Generator, pvcName string, pvcNamespace string) *importer.PodSettings { +// GetPodSettingsWithPVCFunc: func(ownerReference *metav1.OwnerReference, generator *supplements.Generator, s1 string, s2 string) *importer.PodSettings { // panic("mock out the GetPodSettingsWithPVC method") // }, // ProtectFunc: func(ctx context.Context, pod *corev1.Pod) error { @@ -47,7 +63,7 @@ var _ Importer = &ImporterMock{} // StartFunc: func(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error { // panic("mock out the Start method") // }, -// StartWithPodSettingFunc: func(ctx context.Context, settings *importer.Settings, sup *supplements.Generator, caBundle *datasource.CABundle, podSettings *importer.PodSettings) error { +// StartWithPodSettingFunc: func(contextMoqParam context.Context, settings *importer.Settings, generator *supplements.Generator, cABundle *datasource.CABundle, podSettings *importer.PodSettings) error { // panic("mock out the StartWithPodSetting method") // }, // UnprotectFunc: func(ctx context.Context, pod *corev1.Pod) error { @@ -70,7 +86,7 @@ type ImporterMock struct { GetPodFunc func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) // GetPodSettingsWithPVCFunc mocks the GetPodSettingsWithPVC method. - GetPodSettingsWithPVCFunc func(ownerRef *metav1.OwnerReference, sup *supplements.Generator, pvcName string, pvcNamespace string) *importer.PodSettings + GetPodSettingsWithPVCFunc func(ownerReference *metav1.OwnerReference, generator *supplements.Generator, s1 string, s2 string) *importer.PodSettings // ProtectFunc mocks the Protect method. ProtectFunc func(ctx context.Context, pod *corev1.Pod) error @@ -79,7 +95,7 @@ type ImporterMock struct { StartFunc func(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle, opts ...service.Option) error // StartWithPodSettingFunc mocks the StartWithPodSetting method. - StartWithPodSettingFunc func(ctx context.Context, settings *importer.Settings, sup *supplements.Generator, caBundle *datasource.CABundle, podSettings *importer.PodSettings) error + StartWithPodSettingFunc func(contextMoqParam context.Context, settings *importer.Settings, generator *supplements.Generator, cABundle *datasource.CABundle, podSettings *importer.PodSettings) error // UnprotectFunc mocks the Unprotect method. UnprotectFunc func(ctx context.Context, pod *corev1.Pod) error @@ -109,14 +125,14 @@ type ImporterMock struct { } // GetPodSettingsWithPVC holds details about calls to the GetPodSettingsWithPVC method. GetPodSettingsWithPVC []struct { - // OwnerRef is the ownerRef argument value. - OwnerRef *metav1.OwnerReference - // Sup is the sup argument value. - Sup *supplements.Generator - // PvcName is the pvcName argument value. - PvcName string - // PvcNamespace is the pvcNamespace argument value. - PvcNamespace string + // OwnerReference is the ownerReference argument value. + OwnerReference *metav1.OwnerReference + // Generator is the generator argument value. + Generator *supplements.Generator + // S1 is the s1 argument value. + S1 string + // S2 is the s2 argument value. + S2 string } // Protect holds details about calls to the Protect method. Protect []struct { @@ -142,14 +158,14 @@ type ImporterMock struct { } // StartWithPodSetting holds details about calls to the StartWithPodSetting method. StartWithPodSetting []struct { - // Ctx is the ctx argument value. - Ctx context.Context + // ContextMoqParam is the contextMoqParam argument value. + ContextMoqParam context.Context // Settings is the settings argument value. Settings *importer.Settings - // Sup is the sup argument value. - Sup *supplements.Generator - // CaBundle is the caBundle argument value. - CaBundle *datasource.CABundle + // Generator is the generator argument value. + Generator *supplements.Generator + // CABundle is the cABundle argument value. + CABundle *datasource.CABundle // PodSettings is the podSettings argument value. PodSettings *importer.PodSettings } @@ -280,25 +296,25 @@ func (mock *ImporterMock) GetPodCalls() []struct { } // GetPodSettingsWithPVC calls GetPodSettingsWithPVCFunc. -func (mock *ImporterMock) GetPodSettingsWithPVC(ownerRef *metav1.OwnerReference, sup *supplements.Generator, pvcName string, pvcNamespace string) *importer.PodSettings { +func (mock *ImporterMock) GetPodSettingsWithPVC(ownerReference *metav1.OwnerReference, generator *supplements.Generator, s1 string, s2 string) *importer.PodSettings { if mock.GetPodSettingsWithPVCFunc == nil { panic("ImporterMock.GetPodSettingsWithPVCFunc: method is nil but Importer.GetPodSettingsWithPVC was just called") } callInfo := struct { - OwnerRef *metav1.OwnerReference - Sup *supplements.Generator - PvcName string - PvcNamespace string + OwnerReference *metav1.OwnerReference + Generator *supplements.Generator + S1 string + S2 string }{ - OwnerRef: ownerRef, - Sup: sup, - PvcName: pvcName, - PvcNamespace: pvcNamespace, + OwnerReference: ownerReference, + Generator: generator, + S1: s1, + S2: s2, } mock.lockGetPodSettingsWithPVC.Lock() mock.calls.GetPodSettingsWithPVC = append(mock.calls.GetPodSettingsWithPVC, callInfo) mock.lockGetPodSettingsWithPVC.Unlock() - return mock.GetPodSettingsWithPVCFunc(ownerRef, sup, pvcName, pvcNamespace) + return mock.GetPodSettingsWithPVCFunc(ownerReference, generator, s1, s2) } // GetPodSettingsWithPVCCalls gets all the calls that were made to GetPodSettingsWithPVC. @@ -306,16 +322,16 @@ func (mock *ImporterMock) GetPodSettingsWithPVC(ownerRef *metav1.OwnerReference, // // len(mockedImporter.GetPodSettingsWithPVCCalls()) func (mock *ImporterMock) GetPodSettingsWithPVCCalls() []struct { - OwnerRef *metav1.OwnerReference - Sup *supplements.Generator - PvcName string - PvcNamespace string + OwnerReference *metav1.OwnerReference + Generator *supplements.Generator + S1 string + S2 string } { var calls []struct { - OwnerRef *metav1.OwnerReference - Sup *supplements.Generator - PvcName string - PvcNamespace string + OwnerReference *metav1.OwnerReference + Generator *supplements.Generator + S1 string + S2 string } mock.lockGetPodSettingsWithPVC.RLock() calls = mock.calls.GetPodSettingsWithPVC @@ -412,27 +428,27 @@ func (mock *ImporterMock) StartCalls() []struct { } // StartWithPodSetting calls StartWithPodSettingFunc. -func (mock *ImporterMock) StartWithPodSetting(ctx context.Context, settings *importer.Settings, sup *supplements.Generator, caBundle *datasource.CABundle, podSettings *importer.PodSettings) error { +func (mock *ImporterMock) StartWithPodSetting(contextMoqParam context.Context, settings *importer.Settings, generator *supplements.Generator, cABundle *datasource.CABundle, podSettings *importer.PodSettings) error { if mock.StartWithPodSettingFunc == nil { panic("ImporterMock.StartWithPodSettingFunc: method is nil but Importer.StartWithPodSetting was just called") } callInfo := struct { - Ctx context.Context - Settings *importer.Settings - Sup *supplements.Generator - CaBundle *datasource.CABundle - PodSettings *importer.PodSettings + ContextMoqParam context.Context + Settings *importer.Settings + Generator *supplements.Generator + CABundle *datasource.CABundle + PodSettings *importer.PodSettings }{ - Ctx: ctx, - Settings: settings, - Sup: sup, - CaBundle: caBundle, - PodSettings: podSettings, + ContextMoqParam: contextMoqParam, + Settings: settings, + Generator: generator, + CABundle: cABundle, + PodSettings: podSettings, } mock.lockStartWithPodSetting.Lock() mock.calls.StartWithPodSetting = append(mock.calls.StartWithPodSetting, callInfo) mock.lockStartWithPodSetting.Unlock() - return mock.StartWithPodSettingFunc(ctx, settings, sup, caBundle, podSettings) + return mock.StartWithPodSettingFunc(contextMoqParam, settings, generator, cABundle, podSettings) } // StartWithPodSettingCalls gets all the calls that were made to StartWithPodSetting. @@ -440,18 +456,18 @@ func (mock *ImporterMock) StartWithPodSetting(ctx context.Context, settings *imp // // len(mockedImporter.StartWithPodSettingCalls()) func (mock *ImporterMock) StartWithPodSettingCalls() []struct { - Ctx context.Context - Settings *importer.Settings - Sup *supplements.Generator - CaBundle *datasource.CABundle - PodSettings *importer.PodSettings + ContextMoqParam context.Context + Settings *importer.Settings + Generator *supplements.Generator + CABundle *datasource.CABundle + PodSettings *importer.PodSettings } { var calls []struct { - Ctx context.Context - Settings *importer.Settings - Sup *supplements.Generator - CaBundle *datasource.CABundle - PodSettings *importer.PodSettings + ContextMoqParam context.Context + Settings *importer.Settings + Generator *supplements.Generator + CABundle *datasource.CABundle + PodSettings *importer.PodSettings } mock.lockStartWithPodSetting.RLock() calls = mock.calls.StartWithPodSetting @@ -1525,6 +1541,190 @@ func (mock *StatMock) IsUploaderReadyCalls() []struct { return calls } +// Ensure, that BounderMock does implement Bounder. +// If this is not the case, regenerate this file with moq. +var _ Bounder = &BounderMock{} + +// BounderMock is a mock implementation of Bounder. +// +// func TestSomethingThatUsesBounder(t *testing.T) { +// +// // make and configure a mocked Bounder +// mockedBounder := &BounderMock{ +// CleanUpFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// CleanUpSupplementsFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// panic("mock out the CleanUpSupplements method") +// }, +// StartFunc: func(ctx context.Context, ownerRef *metav1.OwnerReference, sup *supplements.Generator, opts ...service.Option) error { +// panic("mock out the Start method") +// }, +// } +// +// // use mockedBounder in code that requires Bounder +// // and then make assertions. +// +// } +type BounderMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup *supplements.Generator) (bool, error) + + // CleanUpSupplementsFunc mocks the CleanUpSupplements method. + CleanUpSupplementsFunc func(ctx context.Context, sup *supplements.Generator) (bool, error) + + // StartFunc mocks the Start method. + StartFunc func(ctx context.Context, ownerRef *metav1.OwnerReference, sup *supplements.Generator, opts ...service.Option) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup *supplements.Generator + } + // CleanUpSupplements holds details about calls to the CleanUpSupplements method. + CleanUpSupplements []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup *supplements.Generator + } + // Start holds details about calls to the Start method. + Start []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // OwnerRef is the ownerRef argument value. + OwnerRef *metav1.OwnerReference + // Sup is the sup argument value. + Sup *supplements.Generator + // Opts is the opts argument value. + Opts []service.Option + } + } + lockCleanUp sync.RWMutex + lockCleanUpSupplements sync.RWMutex + lockStart sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *BounderMock) CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("BounderMock.CleanUpFunc: method is nil but Bounder.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup *supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedBounder.CleanUpCalls()) +func (mock *BounderMock) CleanUpCalls() []struct { + Ctx context.Context + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup *supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// CleanUpSupplements calls CleanUpSupplementsFunc. +func (mock *BounderMock) CleanUpSupplements(ctx context.Context, sup *supplements.Generator) (bool, error) { + if mock.CleanUpSupplementsFunc == nil { + panic("BounderMock.CleanUpSupplementsFunc: method is nil but Bounder.CleanUpSupplements was just called") + } + callInfo := struct { + Ctx context.Context + Sup *supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUpSupplements.Lock() + mock.calls.CleanUpSupplements = append(mock.calls.CleanUpSupplements, callInfo) + mock.lockCleanUpSupplements.Unlock() + return mock.CleanUpSupplementsFunc(ctx, sup) +} + +// CleanUpSupplementsCalls gets all the calls that were made to CleanUpSupplements. +// Check the length with: +// +// len(mockedBounder.CleanUpSupplementsCalls()) +func (mock *BounderMock) CleanUpSupplementsCalls() []struct { + Ctx context.Context + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup *supplements.Generator + } + mock.lockCleanUpSupplements.RLock() + calls = mock.calls.CleanUpSupplements + mock.lockCleanUpSupplements.RUnlock() + return calls +} + +// Start calls StartFunc. +func (mock *BounderMock) Start(ctx context.Context, ownerRef *metav1.OwnerReference, sup *supplements.Generator, opts ...service.Option) error { + if mock.StartFunc == nil { + panic("BounderMock.StartFunc: method is nil but Bounder.Start was just called") + } + callInfo := struct { + Ctx context.Context + OwnerRef *metav1.OwnerReference + Sup *supplements.Generator + Opts []service.Option + }{ + Ctx: ctx, + OwnerRef: ownerRef, + Sup: sup, + Opts: opts, + } + mock.lockStart.Lock() + mock.calls.Start = append(mock.calls.Start, callInfo) + mock.lockStart.Unlock() + return mock.StartFunc(ctx, ownerRef, sup, opts...) +} + +// StartCalls gets all the calls that were made to Start. +// Check the length with: +// +// len(mockedBounder.StartCalls()) +func (mock *BounderMock) StartCalls() []struct { + Ctx context.Context + OwnerRef *metav1.OwnerReference + Sup *supplements.Generator + Opts []service.Option +} { + var calls []struct { + Ctx context.Context + OwnerRef *metav1.OwnerReference + Sup *supplements.Generator + Opts []service.Option + } + mock.lockStart.RLock() + calls = mock.calls.Start + mock.lockStart.RUnlock() + return calls +} + // Ensure, that HandlerMock does implement Handler. // If this is not the case, regenerate this file with moq. var _ Handler = &HandlerMock{} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go index 13b672acb3..3f287539a8 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go @@ -59,16 +59,17 @@ type ObjectRefDataSource struct { storageClassService *service.VirtualImageStorageClassService recorder eventrecord.EventRecorderLogger - viObjectRefOnPvc *ObjectRefDataVirtualImageOnPVC - vdSyncer *ObjectRefVirtualDisk - vdSnapshotSyncer *ObjectRefVirtualDiskSnapshot + viObjectRefOnPvc *ObjectRefDataVirtualImageOnPVC + vdSyncer *ObjectRefVirtualDisk + vdSnapshotCRSyncer *ObjectRefVirtualDiskSnapshotCR + vdSnapshotPVCSyncer *ObjectRefVirtualDiskSnapshotPVC } func NewObjectRefDataSource( recorder eventrecord.EventRecorderLogger, statService Stat, importerService Importer, - bounderService Bounder, + bounderService *service.BounderPodService, dvcrSettings *dvcr.Settings, client client.Client, diskService *service.DiskService, @@ -85,11 +86,16 @@ func NewObjectRefDataSource( recorder: recorder, viObjectRefOnPvc: NewObjectRefDataVirtualImageOnPVC(recorder, statService, importerService, dvcrSettings, client, diskService, storageClassService), vdSyncer: NewObjectRefVirtualDisk(recorder, importerService, client, diskService, dvcrSettings, statService, storageClassService), - vdSnapshotSyncer: NewObjectRefVirtualDiskSnapshot(recorder, importerService, bounderService, client, diskService, dvcrSettings, statService, storageClassService), + vdSnapshotCRSyncer: NewObjectRefVirtualDiskSnapshotCR(importerService, statService, client, dvcrSettings, recorder), + vdSnapshotPVCSyncer: NewObjectRefVirtualDiskSnapshotPVC(importerService, statService, bounderService, client, dvcrSettings, recorder), } } func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + if vi.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualDiskSnapshotKind { + return ds.vdSnapshotPVCSyncer.Sync(ctx, vi) + } + log, ctx := logger.GetDataSourceContext(ctx, objectRefDataSource) condition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) @@ -129,18 +135,6 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *virtv2.Virtual } return ds.vdSyncer.StoreToPVC(ctx, vi, vd, cb) - case virtv2.VirtualDiskSnapshotKind: - vdSnapshotKey := types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace} - vdSnapshot, err := object.FetchObject(ctx, vdSnapshotKey, ds.client, &virtv2.VirtualDiskSnapshot{}) - if err != nil { - return reconcile.Result{}, fmt.Errorf("unable to get VDSnapshot %s: %w", vdSnapshotKey, err) - } - - if vdSnapshot == nil { - return reconcile.Result{}, fmt.Errorf("VDSnapshot object ref %s is nil", vdSnapshotKey) - } - - return ds.vdSnapshotSyncer.StoreToPVC(ctx, vi, vdSnapshot, cb) } supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) @@ -306,6 +300,10 @@ func (ds ObjectRefDataSource) StoreToPVC(ctx context.Context, vi *virtv2.Virtual } func (ds ObjectRefDataSource) StoreToDVCR(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + if vi.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualDiskSnapshotKind { + return ds.vdSnapshotCRSyncer.Sync(ctx, vi) + } + log, ctx := logger.GetDataSourceContext(ctx, "objectref") condition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) @@ -339,18 +337,6 @@ func (ds ObjectRefDataSource) StoreToDVCR(ctx context.Context, vi *virtv2.Virtua } return ds.vdSyncer.StoreToDVCR(ctx, vi, vd, cb) - case virtv2.VirtualDiskSnapshotKind: - vdSnapshotKey := types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace} - vdSnapshot, err := object.FetchObject(ctx, vdSnapshotKey, ds.client, &virtv2.VirtualDiskSnapshot{}) - if err != nil { - return reconcile.Result{}, fmt.Errorf("unable to get VDSnapshot %s: %w", vdSnapshotKey, err) - } - - if vdSnapshot == nil { - return reconcile.Result{}, fmt.Errorf("VDSnapshot object ref %s is nil", vdSnapshotKey) - } - - return ds.vdSnapshotSyncer.StoreToDVCR(ctx, vi, vdSnapshot, cb) } supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) @@ -490,12 +476,17 @@ func (ds ObjectRefDataSource) CleanUp(ctx context.Context, vi *virtv2.VirtualIma return false, err } + bounderRequeue, err := ds.bounderService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + diskRequeue, err := ds.diskService.CleanUp(ctx, supgen) if err != nil { return false, err } - return importerRequeue || diskRequeue, nil + return importerRequeue || bounderRequeue || diskRequeue, nil } func (ds ObjectRefDataSource) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { @@ -546,7 +537,14 @@ func (ds ObjectRefDataSource) Validate(ctx context.Context, vi *virtv2.VirtualIm case virtv2.VirtualImageObjectRefKindVirtualDisk: return ds.vdSyncer.Validate(ctx, vi) case virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot: - return ds.vdSnapshotSyncer.Validate(ctx, vi) + switch vi.Spec.Storage { + case virtv2.StorageKubernetes, virtv2.StoragePersistentVolumeClaim: + return ds.vdSnapshotPVCSyncer.Validate(ctx, vi) + case virtv2.StorageContainerRegistry: + return ds.vdSnapshotCRSyncer.Validate(ctx, vi) + } + + return fmt.Errorf("unexpected object ref kind: %s", vi.Spec.DataSource.ObjectRef.Kind) default: return fmt.Errorf("unexpected object ref kind: %s", vi.Spec.DataSource.ObjectRef.Kind) } diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot.go deleted file mode 100644 index f6e7ec82ca..0000000000 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot.go +++ /dev/null @@ -1,593 +0,0 @@ -/* -Copyright 2024 Flant JSC - -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. -*/ - -package source - -import ( - "context" - "errors" - "fmt" - "strconv" - "strings" - - corev1 "k8s.io/api/core/v1" - storev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/deckhouse/virtualization-controller/pkg/common" - "github.com/deckhouse/virtualization-controller/pkg/common/annotations" - "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - "github.com/deckhouse/virtualization-controller/pkg/common/object" - podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" - "github.com/deckhouse/virtualization-controller/pkg/common/pointer" - "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" - "github.com/deckhouse/virtualization-controller/pkg/controller/importer" - "github.com/deckhouse/virtualization-controller/pkg/controller/service" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/dvcr" - "github.com/deckhouse/virtualization-controller/pkg/eventrecord" - "github.com/deckhouse/virtualization-controller/pkg/logger" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" - "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" -) - -type ObjectRefVirtualDiskSnapshot struct { - importerService Importer - bounderService Bounder - diskService *service.DiskService - statService Stat - dvcrSettings *dvcr.Settings - client client.Client - storageClassService *service.VirtualImageStorageClassService - recorder eventrecord.EventRecorderLogger -} - -func NewObjectRefVirtualDiskSnapshot( - recorder eventrecord.EventRecorderLogger, - importerService Importer, - bounderService Bounder, - client client.Client, - diskService *service.DiskService, - dvcrSettings *dvcr.Settings, - statService Stat, - storageClassService *service.VirtualImageStorageClassService, -) *ObjectRefVirtualDiskSnapshot { - return &ObjectRefVirtualDiskSnapshot{ - importerService: importerService, - bounderService: bounderService, - client: client, - recorder: recorder, - diskService: diskService, - statService: statService, - dvcrSettings: dvcrSettings, - storageClassService: storageClassService, - } -} - -func (ds ObjectRefVirtualDiskSnapshot) StoreToDVCR(ctx context.Context, vi *virtv2.VirtualImage, vdSnapshotRef *virtv2.VirtualDiskSnapshot, cb *conditions.ConditionBuilder) (reconcile.Result, error) { - log, ctx := logger.GetDataSourceContext(ctx, "objectref") - - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vdSnapshotRef.Namespace, vi.UID) - pod, err := ds.importerService.GetPod(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - vs, err := ds.diskService.GetVolumeSnapshot(ctx, vdSnapshotRef.Status.VolumeSnapshotName, vdSnapshotRef.Namespace) - if err != nil { - return reconcile.Result{}, err - } - - condition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) - switch { - case isDiskProvisioningFinished(condition): - log.Info("Virtual image provisioning finished: clean up") - - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - vi.Status.Phase = virtv2.ImageReady - - err = ds.importerService.Unprotect(ctx, pod) - if err != nil { - return reconcile.Result{}, err - } - - return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(pod, pvc): - vi.Status.Phase = virtv2.ImagePending - - log.Info("Cleaning up...") - case pvc == nil: - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - virtv2.ReasonDataSourceSyncStarted, - "The ObjectRef DataSource import has started", - ) - - namespacedName := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID).PersistentVolumeClaim() - - storageClassName := vs.Annotations["storageClass"] - volumeMode := vs.Annotations["volumeMode"] - accessModesStr := strings.Split(vs.Annotations["accessModes"], ",") - accessModes := make([]corev1.PersistentVolumeAccessMode, 0, len(accessModesStr)) - for _, accessModeStr := range accessModesStr { - accessModes = append(accessModes, corev1.PersistentVolumeAccessMode(accessModeStr)) - } - - spec := corev1.PersistentVolumeClaimSpec{ - AccessModes: accessModes, - DataSource: &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To(vs.GroupVersionKind().Group), - Kind: vs.Kind, - Name: vs.Name, - }, - } - - if storageClassName != "" { - spec.StorageClassName = &storageClassName - vi.Status.StorageClassName = storageClassName - } - - if volumeMode != "" { - spec.VolumeMode = ptr.To(corev1.PersistentVolumeMode(volumeMode)) - } - - if vs.Status != nil && vs.Status.RestoreSize != nil { - spec.Resources = corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: *vs.Status.RestoreSize, - }, - } - } - - pvc = &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespacedName.Name, - Namespace: namespacedName.Namespace, - OwnerReferences: []metav1.OwnerReference{ - service.MakeOwnerReference(vi), - }, - }, - Spec: spec, - } - - err = ds.diskService.CreatePersistentVolumeClaim(ctx, pvc) - if err != nil { - setPhaseConditionToFailed(cb, &vi.Status.Phase, err) - return reconcile.Result{}, err - } - - vi.Status.Phase = virtv2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC has created: waiting to be Bound.") - - vi.Status.Progress = "0%" - vi.Status.SourceUID = pointer.GetPointer(vs.UID) - - return reconcile.Result{Requeue: true}, err - case pod == nil: - vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) - vi.Status.Target.RegistryURL = ds.statService.GetDVCRImageName(pod) - - envSettings := ds.getEnvSettings(vi, supgen) - - ownerRef := metav1.NewControllerRef(vi, vi.GroupVersionKind()) - podSettings := ds.importerService.GetPodSettingsWithPVC(ownerRef, supgen, pvc.Name, pvc.Namespace) - err = ds.importerService.StartWithPodSetting(ctx, envSettings, supgen, datasource.NewCABundleForVMI(vi.GetNamespace(), vi.Spec.DataSource), podSettings) - switch { - case err == nil: - // OK. - case common.ErrQuotaExceeded(err): - ds.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vi.Status.Phase, err, vi.CreationTimestamp), nil - default: - setPhaseConditionToFailed(cb, &vi.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, err - } - - vi.Status.Phase = virtv2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("DVCR Provisioner not found: create the new one.") - - log.Info("Create importer pod...", "progress", vi.Status.Progress, "pod.phase", "nil") - - return reconcile.Result{Requeue: true}, nil - case podutil.IsPodComplete(pod): - err = ds.statService.CheckPod(pod) - if err != nil { - vi.Status.Phase = virtv2.ImageFailed - - switch { - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - vi.Status.Phase = virtv2.ImageReady - vi.Status.Size = ds.statService.GetSize(pod) - vi.Status.CDROM = ds.statService.GetCDROM(pod) - vi.Status.Format = ds.statService.GetFormat(pod) - vi.Status.Progress = "100%" - vi.Status.Target.RegistryURL = ds.statService.GetDVCRImageName(pod) - - log.Info("Ready", "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) - default: - err = ds.statService.CheckPod(pod) - if err != nil { - vi.Status.Phase = virtv2.ImageFailed - - switch { - case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): - if strings.Contains(err.Error(), "pod has unbound immediate PersistentVolumeClaims") { - vi.Status.Phase = virtv2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("Waiting for PVC to be bound") - - return reconcile.Result{Requeue: true}, nil - } - - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningNotStarted). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - case errors.Is(err, service.ErrProvisioningFailed): - ds.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceDiskProvisioningFailed, "Disk provisioning failed") - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.ProvisioningFailed). - Message(service.CapitalizeFirstLetter(err.Error() + ".")) - return reconcile.Result{}, nil - default: - return reconcile.Result{}, err - } - } - - err = ds.importerService.Protect(ctx, pod) - if err != nil { - return reconcile.Result{}, err - } - - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("Import is in the process of provisioning to DVCR.") - - vi.Status.Phase = virtv2.ImageProvisioning - vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) - vi.Status.Target.RegistryURL = ds.statService.GetDVCRImageName(pod) - - log.Info("Provisioning...", "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) - } - - return reconcile.Result{Requeue: true}, nil -} - -func (ds ObjectRefVirtualDiskSnapshot) StoreToPVC(ctx context.Context, vi *virtv2.VirtualImage, vdSnapshotRef *virtv2.VirtualDiskSnapshot, cb *conditions.ConditionBuilder) (reconcile.Result, error) { - log, ctx := logger.GetDataSourceContext(ctx, objectRefDataSource) - - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - - pod, err := ds.bounderService.GetPod(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - vs, err := ds.diskService.GetVolumeSnapshot(ctx, vdSnapshotRef.Status.VolumeSnapshotName, vdSnapshotRef.Namespace) - if err != nil { - return reconcile.Result{}, err - } - - clusterDefaultSC, _ := ds.diskService.GetDefaultStorageClass(ctx) - sc, err := ds.storageClassService.GetStorageClass(vi.Spec.PersistentVolumeClaim.StorageClass, clusterDefaultSC) - if updated, err := setConditionFromStorageClassError(err, cb); err != nil || updated { - return reconcile.Result{}, err - } - - storageClass, err := ds.diskService.GetStorageClass(ctx, sc) - if err != nil { - return reconcile.Result{}, err - } - - condition, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) - switch { - case isDiskProvisioningFinished(condition): - log.Info("Disk provisioning finished: clean up") - - setPhaseConditionForFinishedImage(pvc, cb, &vi.Status.Phase, supgen) - - // Protect Ready Disk and underlying PVC. - err = ds.diskService.Protect(ctx, vi, nil, pvc) - if err != nil { - return reconcile.Result{}, err - } - - err = ds.bounderService.Unprotect(ctx, pod) - if err != nil { - return reconcile.Result{}, err - } - - return CleanUpSupplements(ctx, vi, ds) - case object.AnyTerminating(pvc, pod): - log.Info("Waiting for supplements to be terminated") - case pvc == nil: - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - virtv2.ReasonDataSourceSyncStarted, - "The ObjectRef DataSource import has started", - ) - - namespacedName := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID).PersistentVolumeClaim() - - storageClassName := vs.Annotations["storageClass"] - volumeMode := vs.Annotations["volumeMode"] - accessModesStr := strings.Split(vs.Annotations["accessModes"], ",") - accessModes := make([]corev1.PersistentVolumeAccessMode, 0, len(accessModesStr)) - for _, accessModeStr := range accessModesStr { - accessModes = append(accessModes, corev1.PersistentVolumeAccessMode(accessModeStr)) - } - - spec := corev1.PersistentVolumeClaimSpec{ - AccessModes: accessModes, - DataSource: &corev1.TypedLocalObjectReference{ - APIGroup: ptr.To(vs.GroupVersionKind().Group), - Kind: vs.Kind, - Name: vs.Name, - }, - } - - if storageClassName != "" { - spec.StorageClassName = &storageClassName - vi.Status.StorageClassName = storageClassName - } - - if volumeMode != "" { - spec.VolumeMode = ptr.To(corev1.PersistentVolumeMode(volumeMode)) - } - - if vs.Status != nil && vs.Status.RestoreSize != nil { - spec.Resources = corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: *vs.Status.RestoreSize, - }, - } - } - - pvc = &corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespacedName.Name, - Namespace: namespacedName.Namespace, - OwnerReferences: []metav1.OwnerReference{ - service.MakeOwnerReference(vi), - }, - }, - Spec: spec, - } - - err = ds.diskService.CreatePersistentVolumeClaim(ctx, pvc) - if err != nil { - setPhaseConditionToFailed(cb, &vi.Status.Phase, err) - return reconcile.Result{}, err - } - - vi.Status.Phase = virtv2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("PVC has created: waiting to be Bound.") - - vi.Status.Progress = "0%" - vi.Status.SourceUID = pointer.GetPointer(vs.UID) - vi.Status.Target.PersistentVolumeClaim = pvc.Name - - return reconcile.Result{Requeue: true}, err - case pvc.Status.Phase == corev1.ClaimPending: - isWFFC := storageClass != nil && storageClass.VolumeBindingMode != nil && *storageClass.VolumeBindingMode == storev1.VolumeBindingWaitForFirstConsumer - - if !isWFFC { - return reconcile.Result{Requeue: true}, nil - } - - ownerRef := metav1.NewControllerRef(vi, vi.GroupVersionKind()) - err = ds.bounderService.Start(ctx, ownerRef, supgen, pvc) - switch { - case err == nil: - // OK. - case common.ErrQuotaExceeded(err): - ds.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") - return setQuotaExceededPhaseCondition(cb, &vi.Status.Phase, err, vi.CreationTimestamp), nil - default: - setPhaseConditionToFailed(cb, &vi.Status.Phase, fmt.Errorf("unexpected error: %w", err)) - return reconcile.Result{}, err - } - - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("Bounder pod has created: waiting to be Bound.") - - return reconcile.Result{Requeue: true}, err - case pvc.Status.Phase == corev1.ClaimBound: - ds.recorder.Event( - vi, - corev1.EventTypeNormal, - virtv2.ReasonDataSourceSyncCompleted, - "The ObjectRef DataSource import has completed", - ) - - vi.Status.Phase = virtv2.ImageReady - cb. - Status(metav1.ConditionTrue). - Reason(vicondition.Ready). - Message("") - - q, err := resource.ParseQuantity(vs.Status.RestoreSize.String()) - if err != nil { - return reconcile.Result{}, err - } - - intQ, ok := q.AsInt64() - if !ok { - return reconcile.Result{}, errors.New("fail to convert quantity to int64") - } - - vi.Status.Size = virtv2.ImageStatusSize{ - Stored: vs.Status.RestoreSize.String(), - StoredBytes: strconv.FormatInt(intQ, 10), - Unpacked: vs.Status.RestoreSize.String(), - UnpackedBytes: strconv.FormatInt(intQ, 10), - } - - vi.Status.Progress = "100%" - default: - vi.Status.Phase = virtv2.ImageProvisioning - cb. - Status(metav1.ConditionFalse). - Reason(vicondition.Provisioning). - Message("Import is in the process of provisioning to PVC.") - - return reconcile.Result{}, nil - } - - return reconcile.Result{Requeue: true}, nil -} - -func (ds ObjectRefVirtualDiskSnapshot) CleanUpSupplements(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - - importerRequeue, err := ds.importerService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - diskRequeue, err := ds.diskService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - bounderRequeue, err := ds.bounderService.CleanUpSupplements(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - if vi.Spec.Storage == virtv2.StorageContainerRegistry { - pvcCleanupRequeue, err := ds.diskService.CleanUp(ctx, supgen) - if err != nil { - return reconcile.Result{}, err - } - - return reconcile.Result{Requeue: pvcCleanupRequeue}, nil - } - - return reconcile.Result{Requeue: importerRequeue || diskRequeue || bounderRequeue}, nil -} - -func (ds ObjectRefVirtualDiskSnapshot) CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { - supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) - - importerRequeue, err := ds.importerService.CleanUp(ctx, supgen) - if err != nil { - return false, err - } - - diskRequeue, err := ds.diskService.CleanUp(ctx, supgen) - if err != nil { - return false, err - } - - bounderRequeue, err := ds.bounderService.CleanUpSupplements(ctx, supgen) - if err != nil { - return false, err - } - - return importerRequeue || diskRequeue || bounderRequeue, nil -} - -func (ds ObjectRefVirtualDiskSnapshot) getEnvSettings(vi *virtv2.VirtualImage, sup *supplements.Generator) *importer.Settings { - var settings importer.Settings - importer.ApplyBlockDeviceSourceSettings(&settings) - importer.ApplyDVCRDestinationSettings( - &settings, - ds.dvcrSettings, - sup, - ds.dvcrSettings.RegistryImageForVI(vi), - ) - - return &settings -} - -func (ds ObjectRefVirtualDiskSnapshot) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { - if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot { - return fmt.Errorf("not a %s data source", virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot) - } - - vdSnapshot, err := ds.diskService.GetVirtualDiskSnapshot(ctx, vi.Spec.DataSource.ObjectRef.Name, vi.Namespace) - if err != nil { - return err - } - - if vdSnapshot == nil || vdSnapshot.Status.Phase != virtv2.VirtualDiskSnapshotPhaseReady { - return NewVirtualDiskSnapshotNotReadyError(vi.Spec.DataSource.ObjectRef.Name) - } - - volumeSnapshot, err := ds.diskService.GetVolumeSnapshot(ctx, vdSnapshot.Status.VolumeSnapshotName, vdSnapshot.Namespace) - if err != nil { - return err - } - - if volumeSnapshot == nil || !*volumeSnapshot.Status.ReadyToUse { - return NewVirtualDiskSnapshotNotReadyError(vi.Spec.DataSource.ObjectRef.Name) - } - - return nil -} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr.go new file mode 100644 index 0000000000..124b456798 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr.go @@ -0,0 +1,124 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package source + +import ( + "context" + "errors" + "fmt" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/blockdevice" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source/step" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type ObjectRefVirtualDiskSnapshotCR struct { + importer Importer + stat Stat + client client.Client + dvcrSettings *dvcr.Settings + recorder eventrecord.EventRecorderLogger +} + +func NewObjectRefVirtualDiskSnapshotCR( + importer Importer, + statService Stat, + client client.Client, + dvcrSettings *dvcr.Settings, + recorder eventrecord.EventRecorderLogger, +) *ObjectRefVirtualDiskSnapshotCR { + return &ObjectRefVirtualDiskSnapshotCR{ + importer: importer, + client: client, + recorder: recorder, + stat: statService, + dvcrSettings: dvcrSettings, + } +} + +func (ds ObjectRefVirtualDiskSnapshotCR) Sync(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot { + return reconcile.Result{}, errors.New("object ref missed for data source") + } + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + cb := conditions.NewConditionBuilder(vicondition.ReadyType).Generation(vi.Generation) + defer func() { conditions.SetCondition(cb, &vi.Status.Conditions) }() + + pvc, err := object.FetchObject(ctx, supgen.PersistentVolumeClaim(), ds.client, &corev1.PersistentVolumeClaim{}) + if err != nil { + return reconcile.Result{}, fmt.Errorf("fetch pvc: %w", err) + } + + pod, err := importer.FindPod(ctx, ds.client, supgen) + if err != nil { + return reconcile.Result{}, fmt.Errorf("fetch pod: %w", err) + } + + return blockdevice.NewStepTakers[*virtv2.VirtualImage]( + step.NewReadyContainerRegistryStep(pod, ds.importer, ds.stat, ds.recorder, cb), + step.NewTerminatingStep(pvc), + step.NewCreatePersistentVolumeClaimStep(pvc, ds.recorder, ds.client, cb), + step.NewCreatePodStep(pod, ds.dvcrSettings, ds.recorder, ds.importer, ds.stat, cb), + step.NewWaitForPodStep(pod, pvc, ds.stat, cb), + ).Run(ctx, vi) +} + +func (ds ObjectRefVirtualDiskSnapshotCR) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { + return validateVirtualDiskSnapshot(ctx, vi, ds.client) +} + +func validateVirtualDiskSnapshot(ctx context.Context, vi *virtv2.VirtualImage, client client.Client) error { + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot { + return errors.New("object ref missed for data source") + } + + vdSnapshot, err := object.FetchObject(ctx, types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace}, client, &virtv2.VirtualDiskSnapshot{}) + if err != nil { + return fmt.Errorf("fetch virtual disk snapshot: %w", err) + } + + if vdSnapshot == nil || vdSnapshot.Status.Phase != virtv2.VirtualDiskSnapshotPhaseReady { + return NewVirtualDiskSnapshotNotReadyError(vi.Spec.DataSource.ObjectRef.Name) + } + + vs, err := object.FetchObject(ctx, types.NamespacedName{Name: vdSnapshot.Status.VolumeSnapshotName, Namespace: vdSnapshot.Namespace}, client, &vsv1.VolumeSnapshot{}) + if err != nil { + return fmt.Errorf("fetch volume snapshot: %w", err) + } + + if vs == nil || vs.Status.ReadyToUse == nil || !*vs.Status.ReadyToUse { + return NewVirtualDiskSnapshotNotReadyError(vi.Spec.DataSource.ObjectRef.Name) + } + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr_test.go new file mode 100644 index 0000000000..7991056425 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_cr_test.go @@ -0,0 +1,313 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package source + +import ( + "context" + "log/slog" + "testing" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + importer2 "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +func TestHandlers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Sources") +} + +var _ = Describe("ObjectRef VirtualImageSnapshot ContainerRegistry", func() { + var ( + ctx context.Context + scheme *runtime.Scheme + vi *virtv2.VirtualImage + vs *vsv1.VolumeSnapshot + sc *storagev1.StorageClass + vdSnapshot *virtv2.VirtualDiskSnapshot + pvc *corev1.PersistentVolumeClaim + pod *corev1.Pod + settings *dvcr.Settings + recorder eventrecord.EventRecorderLogger + importer *ImporterMock + stat *StatMock + ) + + BeforeEach(func() { + ctx = logger.ToContext(context.TODO(), slog.Default()) + + scheme = runtime.NewScheme() + Expect(virtv2.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + Expect(vsv1.AddToScheme(scheme)).To(Succeed()) + Expect(storagev1.AddToScheme(scheme)).To(Succeed()) + + recorder = &eventrecord.EventRecorderLoggerMock{ + EventFunc: func(_ client.Object, _, _, _ string) {}, + } + + importer = &ImporterMock{ + CleanUpSupplementsFunc: func(_ context.Context, _ *supplements.Generator) (bool, error) { + return false, nil + }, + } + stat = &StatMock{ + GetDVCRImageNameFunc: func(_ *corev1.Pod) string { + return "image" + }, + CheckPodFunc: func(_ *corev1.Pod) error { + return nil + }, + GetSizeFunc: func(_ *corev1.Pod) virtv2.ImageStatusSize { + return virtv2.ImageStatusSize{} + }, + GetCDROMFunc: func(_ *corev1.Pod) bool { + return false + }, + GetFormatFunc: func(_ *corev1.Pod) string { + return "iso" + }, + GetProgressFunc: func(_ types.UID, _ *corev1.Pod, _ string, _ ...service.GetProgressOption) string { + return "N%" + }, + } + settings = &dvcr.Settings{} + + sc = &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sc", + }, + } + + vs = &vsv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + }, + Status: &vsv1.VolumeSnapshotStatus{ + ReadyToUse: ptr.To(true), + }, + } + + vdSnapshot = &virtv2.VirtualDiskSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vd-snapshot", + UID: "11111111-1111-1111-1111-111111111111", + }, + Spec: virtv2.VirtualDiskSnapshotSpec{}, + Status: virtv2.VirtualDiskSnapshotStatus{ + Phase: virtv2.VirtualDiskSnapshotPhaseReady, + VolumeSnapshotName: vs.Name, + }, + } + + vi = &virtv2.VirtualImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vi", + Generation: 1, + UID: "22222222-2222-2222-2222-222222222222", + }, + Spec: virtv2.VirtualImageSpec{ + DataSource: virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot, + Name: vdSnapshot.Name, + }, + }, + }, + } + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + pvc = &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: supgen.PersistentVolumeClaim().Name, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: &sc.Name, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + }, + } + + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: supgen.ImporterPod().Name, + }, + } + }) + + Context("VirtualImage has just been created", func() { + It("must create PVC and Pod", func() { + var pvcCreated bool + var podCreated bool + + importer.GetPodSettingsWithPVCFunc = func(_ *metav1.OwnerReference, _ *supplements.Generator, _, _ string) *importer2.PodSettings { + return nil + } + importer.StartWithPodSettingFunc = func(_ context.Context, _ *importer2.Settings, _ *supplements.Generator, _ *datasource.CABundle, _ *importer2.PodSettings) error { + podCreated = true + return nil + } + + vi.Status = virtv2.VirtualImageStatus{} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vdSnapshot, vs). + WithInterceptorFuncs(interceptor.Funcs{ + Create: func(_ context.Context, _ client.WithWatch, obj client.Object, _ ...client.CreateOption) error { + switch obj.(type) { + case *corev1.PersistentVolumeClaim: + pvcCreated = true + } + + return nil + }, + }).Build() + + syncer := NewObjectRefVirtualDiskSnapshotCR(importer, stat, client, settings, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(pvcCreated).To(BeTrue()) + Expect(podCreated).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.SourceUID).ToNot(BeNil()) + Expect(*vi.Status.SourceUID).ToNot(BeEmpty()) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + Expect(vi.Status.Target.PersistentVolumeClaim).To(BeEmpty()) + }) + }) + + Context("VirtualImage waits for the Pod to be Completed", func() { + It("waits for the PVC to be Bound", func() { + pvc.Status.Phase = corev1.ClaimPending + pod.Status.Phase = corev1.PodPending + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, pod).Build() + + syncer := NewObjectRefVirtualDiskSnapshotCR(importer, stat, client, nil, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + }) + + It("waits for the Pod to be Running", func() { + pod.Status.Phase = corev1.PodPending + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, pod).Build() + + syncer := NewObjectRefVirtualDiskSnapshotCR(importer, stat, client, nil, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + }) + + It("waits for the Pod to be Succeeded", func() { + pod.Status.Phase = corev1.PodRunning + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc, pod).Build() + + syncer := NewObjectRefVirtualDiskSnapshotCR(importer, stat, client, nil, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.RequeueAfter).ToNot(BeZero()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + }) + }) + + Context("VirtualImage is ready", func() { + It("has Pod in Succeeded phase", func() { + pod.Status.Phase = corev1.PodSucceeded + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pod).Build() + + syncer := NewObjectRefVirtualDiskSnapshotCR(importer, stat, client, nil, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionTrue, vicondition.Ready, false) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageReady)) + }) + + It("does not have Pod", func() { + vi.Status.Conditions = []metav1.Condition{ + { + Type: vicondition.ReadyType.String(), + Status: metav1.ConditionTrue, + Reason: vicondition.Ready.String(), + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + syncer := NewObjectRefVirtualDiskSnapshotCR(importer, stat, client, nil, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionTrue, vicondition.Ready, false) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageReady)) + }) + }) +}) + +func ExpectCondition(vi *virtv2.VirtualImage, status metav1.ConditionStatus, reason vicondition.ReadyReason, msgExists bool) { + ready, _ := conditions.GetCondition(vicondition.Ready, vi.Status.Conditions) + Expect(ready.Status).To(Equal(status)) + Expect(ready.Reason).To(Equal(reason.String())) + Expect(ready.ObservedGeneration).To(Equal(vi.Generation)) + + if msgExists { + Expect(ready.Message).ToNot(BeEmpty()) + } else { + Expect(ready.Message).To(BeEmpty()) + } +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc.go new file mode 100644 index 0000000000..9ae1a2d0eb --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc.go @@ -0,0 +1,93 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package source + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/blockdevice" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source/step" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type ObjectRefVirtualDiskSnapshotPVC struct { + importer Importer + stat Stat + bounder Bounder + client client.Client + dvcrSettings *dvcr.Settings + recorder eventrecord.EventRecorderLogger +} + +func NewObjectRefVirtualDiskSnapshotPVC( + importer Importer, + stat Stat, + bounder Bounder, + client client.Client, + dvcrSettings *dvcr.Settings, + recorder eventrecord.EventRecorderLogger, +) *ObjectRefVirtualDiskSnapshotPVC { + return &ObjectRefVirtualDiskSnapshotPVC{ + importer: importer, + stat: stat, + bounder: bounder, + client: client, + dvcrSettings: dvcrSettings, + recorder: recorder, + } +} + +func (ds ObjectRefVirtualDiskSnapshotPVC) Sync(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + if vi.Spec.DataSource.ObjectRef == nil || vi.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot { + return reconcile.Result{}, errors.New("object ref missed for data source") + } + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + cb := conditions.NewConditionBuilder(vicondition.ReadyType).Generation(vi.Generation) + defer func() { conditions.SetCondition(cb, &vi.Status.Conditions) }() + + pvc, err := object.FetchObject(ctx, supgen.PersistentVolumeClaim(), ds.client, &corev1.PersistentVolumeClaim{}) + if err != nil { + return reconcile.Result{}, fmt.Errorf("fetch pvc: %w", err) + } + + return blockdevice.NewStepTakers[*virtv2.VirtualImage]( + step.NewReadyPersistentVolumeClaimStep(pvc, ds.recorder, cb), + step.NewTerminatingStep(pvc), + step.NewCreatePersistentVolumeClaimStep(pvc, ds.recorder, ds.client, cb), + step.NewCreateBounderPodStep(pvc, ds.bounder, ds.client, ds.recorder, cb), + step.NewWaitForPVCStep(pvc, cb), + ).Run(ctx, vi) +} + +func (ds ObjectRefVirtualDiskSnapshotPVC) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { + return validateVirtualDiskSnapshot(ctx, vi, ds.client) +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go new file mode 100644 index 0000000000..f1b5779139 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go @@ -0,0 +1,242 @@ +/* +Copyright 2024 Flant JSC + +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. +*/ + +package source + +import ( + "context" + "log/slog" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() { + var ( + ctx context.Context + scheme *runtime.Scheme + vi *virtv2.VirtualImage + vs *vsv1.VolumeSnapshot + sc *storagev1.StorageClass + vdSnapshot *virtv2.VirtualDiskSnapshot + pvc *corev1.PersistentVolumeClaim + settings *dvcr.Settings + recorder eventrecord.EventRecorderLogger + importer *ImporterMock + stat *StatMock + ) + + BeforeEach(func() { + ctx = logger.ToContext(context.TODO(), slog.Default()) + + scheme = runtime.NewScheme() + Expect(virtv2.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + Expect(vsv1.AddToScheme(scheme)).To(Succeed()) + Expect(storagev1.AddToScheme(scheme)).To(Succeed()) + + recorder = &eventrecord.EventRecorderLoggerMock{ + EventFunc: func(_ client.Object, _, _, _ string) {}, + } + + importer = &ImporterMock{ + CleanUpSupplementsFunc: func(_ context.Context, _ *supplements.Generator) (bool, error) { + return false, nil + }, + } + stat = &StatMock{ + GetDVCRImageNameFunc: func(_ *corev1.Pod) string { + return "image" + }, + CheckPodFunc: func(_ *corev1.Pod) error { + return nil + }, + GetSizeFunc: func(_ *corev1.Pod) virtv2.ImageStatusSize { + return virtv2.ImageStatusSize{} + }, + GetCDROMFunc: func(_ *corev1.Pod) bool { + return false + }, + GetFormatFunc: func(_ *corev1.Pod) string { + return "iso" + }, + GetProgressFunc: func(_ types.UID, _ *corev1.Pod, _ string, _ ...service.GetProgressOption) string { + return "N%" + }, + } + settings = &dvcr.Settings{} + + sc = &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sc", + }, + } + + vs = &vsv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + }, + Status: &vsv1.VolumeSnapshotStatus{ + ReadyToUse: ptr.To(true), + }, + } + + vdSnapshot = &virtv2.VirtualDiskSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vd-snapshot", + UID: "11111111-1111-1111-1111-111111111111", + }, + Spec: virtv2.VirtualDiskSnapshotSpec{}, + Status: virtv2.VirtualDiskSnapshotStatus{ + Phase: virtv2.VirtualDiskSnapshotPhaseReady, + VolumeSnapshotName: vs.Name, + }, + } + + vi = &virtv2.VirtualImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vi", + Generation: 1, + UID: "22222222-2222-2222-2222-222222222222", + }, + Spec: virtv2.VirtualImageSpec{ + Storage: virtv2.StoragePersistentVolumeClaim, + DataSource: virtv2.VirtualImageDataSource{ + Type: virtv2.DataSourceTypeObjectRef, + ObjectRef: &virtv2.VirtualImageObjectRef{ + Kind: virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot, + Name: vdSnapshot.Name, + }, + }, + }, + } + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + pvc = &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: supgen.PersistentVolumeClaim().Name, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: &sc.Name, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + }, + } + }) + + Context("VirtualImage has just been created", func() { + It("must create PVC", func() { + var pvcCreated bool + + vi.Status = virtv2.VirtualImageStatus{} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vdSnapshot, vs). + WithInterceptorFuncs(interceptor.Funcs{ + Create: func(_ context.Context, _ client.WithWatch, obj client.Object, _ ...client.CreateOption) error { + switch obj.(type) { + case *corev1.PersistentVolumeClaim: + pvcCreated = true + } + + return nil + }, + }).Build() + + syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, settings, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + Expect(pvcCreated).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Provisioning, true) + Expect(vi.Status.SourceUID).ToNot(BeNil()) + Expect(*vi.Status.SourceUID).ToNot(BeEmpty()) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) + Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) + }) + }) + + Context("VirtualImage is ready", func() { + It("has PVC in Bound phase", func() { + pvc.Status.Phase = corev1.ClaimBound + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + + syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, nil, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionTrue, vicondition.Ready, false) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageReady)) + }) + }) + + Context("VirtualImage is lost", func() { + It("is lost when PVC is not found", func() { + vi.Status.Target.PersistentVolumeClaim = pvc.Name + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, nil, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Lost, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageLost)) + Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) + }) + + It("is lost when PVC is lost as well", func() { + pvc.Status.Phase = corev1.ClaimLost + vi.Status.Target.PersistentVolumeClaim = pvc.Name + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pvc).Build() + + syncer := NewObjectRefVirtualDiskSnapshotPVC(importer, stat, nil, client, nil, recorder) + + res, err := syncer.Sync(ctx, vi) + Expect(err).ToNot(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + + ExpectCondition(vi, metav1.ConditionFalse, vicondition.Lost, true) + Expect(vi.Status.Phase).To(Equal(virtv2.ImageLost)) + Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_bounder_pod_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_bounder_pod_step.go new file mode 100644 index 0000000000..71c0754181 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_bounder_pod_step.go @@ -0,0 +1,123 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package step + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type CreateBounderPodStepBounder interface { + Start(ctx context.Context, ownerRef *metav1.OwnerReference, sup *supplements.Generator, opts ...service.Option) error +} + +type CreateBounderPodStep struct { + pvc *corev1.PersistentVolumeClaim + bounder CreateBounderPodStepBounder + client client.Client + recorder eventrecord.EventRecorderLogger + cb *conditions.ConditionBuilder +} + +func NewCreateBounderPodStep( + pvc *corev1.PersistentVolumeClaim, + bounder CreateBounderPodStepBounder, + client client.Client, + recorder eventrecord.EventRecorderLogger, + cb *conditions.ConditionBuilder, +) *CreateBounderPodStep { + return &CreateBounderPodStep{ + pvc: pvc, + bounder: bounder, + client: client, + recorder: recorder, + cb: cb, + } +} + +func (s CreateBounderPodStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.pvc == nil { + return nil, nil + } + + wffc, err := s.isWFFC(ctx) + if err != nil { + return nil, err + } + + if !wffc { + return nil, nil + } + + ownerRef := metav1.NewControllerRef(vi, vi.GroupVersionKind()) + + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + err = s.bounder.Start(ctx, ownerRef, supgen) + switch { + case err == nil: + // OK. + case common.ErrQuotaExceeded(err): + s.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") + return setQuotaExceededPhaseCondition(s.cb, &vi.Status.Phase, err, vi.CreationTimestamp), nil + default: + setPhaseConditionToFailed(s.cb, &vi.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + return nil, err + } + + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Bounder pod has created: waiting to be Bound.") + + return nil, nil +} + +func (s CreateBounderPodStep) isWFFC(ctx context.Context) (bool, error) { + if s.pvc.Spec.StorageClassName == nil || *s.pvc.Spec.StorageClassName == "" { + return false, nil + } + + scKey := types.NamespacedName{Name: *s.pvc.Spec.StorageClassName} + sc, err := object.FetchObject(ctx, scKey, s.client, &storagev1.StorageClass{}) + if err != nil { + return false, fmt.Errorf("fetch storage class: %w", err) + } + + if sc == nil || sc.VolumeBindingMode == nil || *sc.VolumeBindingMode != storagev1.VolumeBindingWaitForFirstConsumer { + return false, nil + } + + return true, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pod_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pod_step.go new file mode 100644 index 0000000000..fd1ab6071c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pod_step.go @@ -0,0 +1,150 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package step + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type CreatePodStepImporter interface { + GetPodSettingsWithPVC(_ *metav1.OwnerReference, _ *supplements.Generator, _, _ string) *importer.PodSettings + StartWithPodSetting(_ context.Context, _ *importer.Settings, _ *supplements.Generator, _ *datasource.CABundle, _ *importer.PodSettings) error +} + +type CreatePodStepStat interface { + GetSize(pod *corev1.Pod) virtv2.ImageStatusSize + GetDVCRImageName(pod *corev1.Pod) string + GetFormat(pod *corev1.Pod) string + GetCDROM(pod *corev1.Pod) bool +} + +type CreatePodStep struct { + pod *corev1.Pod + dvcrSettings *dvcr.Settings + recorder eventrecord.EventRecorderLogger + importer CreatePodStepImporter + stat CreatePodStepStat + cb *conditions.ConditionBuilder +} + +func NewCreatePodStep( + pod *corev1.Pod, + dvcrSettings *dvcr.Settings, + recorder eventrecord.EventRecorderLogger, + importer CreatePodStepImporter, + stat CreatePodStepStat, + cb *conditions.ConditionBuilder, +) *CreatePodStep { + return &CreatePodStep{ + pod: pod, + dvcrSettings: dvcrSettings, + recorder: recorder, + importer: importer, + stat: stat, + cb: cb, + } +} + +func (s CreatePodStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.pod != nil { + return nil, nil + } + + ownerRef := metav1.NewControllerRef(vi, vi.GroupVersionKind()) + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + pvcKey := supgen.PersistentVolumeClaim() + podSettings := s.importer.GetPodSettingsWithPVC(ownerRef, supgen, pvcKey.Name, pvcKey.Namespace) + + envSettings := s.getEnvSettings(vi, supgen) + + err := s.importer.StartWithPodSetting(ctx, envSettings, supgen, datasource.NewCABundleForVMI(vi.GetNamespace(), vi.Spec.DataSource), podSettings) + switch { + case err == nil: + // OK. + case common.ErrQuotaExceeded(err): + s.recorder.Event(vi, corev1.EventTypeWarning, virtv2.ReasonDataSourceQuotaExceeded, "DataSource quota exceed") + return setQuotaExceededPhaseCondition(s.cb, &vi.Status.Phase, err, vi.CreationTimestamp), nil + default: + setPhaseConditionToFailed(s.cb, &vi.Status.Phase, fmt.Errorf("unexpected error: %w", err)) + return nil, err + } + + log, _ := logger.GetDataSourceContext(ctx, "objectref") + log.Debug("The importer Pod has just been created.") + + vi.Status.Progress = "0%" + vi.Status.Target.RegistryURL = s.stat.GetDVCRImageName(s.pod) + + return nil, nil +} + +func (s CreatePodStep) getEnvSettings(vi *virtv2.VirtualImage, sup *supplements.Generator) *importer.Settings { + var settings importer.Settings + importer.ApplyBlockDeviceSourceSettings(&settings) + importer.ApplyDVCRDestinationSettings( + &settings, + s.dvcrSettings, + sup, + s.dvcrSettings.RegistryImageForVI(vi), + ) + + return &settings +} + +const retryPeriod = 1 + +func setQuotaExceededPhaseCondition(cb *conditions.ConditionBuilder, phase *virtv2.ImagePhase, err error, creationTimestamp metav1.Time) *reconcile.Result { + *phase = virtv2.ImageFailed + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed) + + if creationTimestamp.Add(30 * time.Minute).After(time.Now()) { + cb.Message(fmt.Sprintf("Quota exceeded: %s; Please configure quotas or try recreating the resource later.", err)) + return &reconcile.Result{} + } + + cb.Message(fmt.Sprintf("Quota exceeded: %s; Retry in %d minute.", err, retryPeriod)) + return &reconcile.Result{RequeueAfter: retryPeriod * time.Minute} +} + +func setPhaseConditionToFailed(cb *conditions.ConditionBuilder, phase *virtv2.ImagePhase, err error) { + *phase = virtv2.ImageFailed + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(err.Error())) +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pvc_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pvc_step.go new file mode 100644 index 0000000000..a8202e285a --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/create_pvc_step.go @@ -0,0 +1,176 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package step + +import ( + "context" + "fmt" + "strings" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/common/pointer" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type CreatePersistentVolumeClaimStep struct { + pvc *corev1.PersistentVolumeClaim + recorder eventrecord.EventRecorderLogger + client client.Client + cb *conditions.ConditionBuilder +} + +func NewCreatePersistentVolumeClaimStep( + pvc *corev1.PersistentVolumeClaim, + recorder eventrecord.EventRecorderLogger, + client client.Client, + cb *conditions.ConditionBuilder, +) *CreatePersistentVolumeClaimStep { + return &CreatePersistentVolumeClaimStep{ + pvc: pvc, + recorder: recorder, + client: client, + cb: cb, + } +} + +func (s CreatePersistentVolumeClaimStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.pvc != nil { + return nil, nil + } + + s.recorder.Event( + vi, + corev1.EventTypeNormal, + virtv2.ReasonDataSourceSyncStarted, + "The ObjectRef DataSource import has started", + ) + + vdSnapshot, err := object.FetchObject(ctx, types.NamespacedName{Name: vi.Spec.DataSource.ObjectRef.Name, Namespace: vi.Namespace}, s.client, &virtv2.VirtualDiskSnapshot{}) + if err != nil { + return nil, fmt.Errorf("fetch virtual disk snapshot: %w", err) + } + + if vdSnapshot == nil { + vi.Status.Phase = virtv2.ImagePending + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningNotStarted). + Message(fmt.Sprintf("VirtualDiskSnapshot %q not found.", vi.Spec.DataSource.ObjectRef.Name)) + return &reconcile.Result{}, nil + } + + vs, err := object.FetchObject(ctx, types.NamespacedName{Name: vdSnapshot.Status.VolumeSnapshotName, Namespace: vdSnapshot.Namespace}, s.client, &vsv1.VolumeSnapshot{}) + if err != nil { + return nil, fmt.Errorf("fetch volume snapshot: %w", err) + } + + if vdSnapshot.Status.Phase != virtv2.VirtualDiskSnapshotPhaseReady || vs == nil || vs.Status == nil || vs.Status.ReadyToUse == nil || !*vs.Status.ReadyToUse { + vi.Status.Phase = virtv2.ImagePending + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningNotStarted). + Message(fmt.Sprintf("VirtualDiskSnapshot %q is not ready to use.", vdSnapshot.Name)) + return &reconcile.Result{}, nil + } + + pvc := s.buildPVC(vi, vs) + + err = s.client.Create(ctx, pvc) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return nil, fmt.Errorf("create pvc: %w", err) + } + + log, _ := logger.GetDataSourceContext(ctx, "objectref") + log.With("pvc.name", pvc.Name).Debug("The underlying PVC has just been created.") + + if vi.Spec.Storage == virtv2.StoragePersistentVolumeClaim || vi.Spec.Storage == virtv2.StorageKubernetes { + vi.Status.Target.PersistentVolumeClaim = pvc.Name + } + + vi.Status.Progress = "0%" + vi.Status.SourceUID = pointer.GetPointer(vdSnapshot.UID) + + return nil, nil +} + +func (s CreatePersistentVolumeClaimStep) buildPVC(vi *virtv2.VirtualImage, vs *vsv1.VolumeSnapshot) *corev1.PersistentVolumeClaim { + storageClassName := vs.Annotations["storageClass"] + volumeMode := vs.Annotations["volumeMode"] + accessModesStr := strings.Split(vs.Annotations["accessModes"], ",") + accessModes := make([]corev1.PersistentVolumeAccessMode, 0, len(accessModesStr)) + for _, accessModeStr := range accessModesStr { + accessModes = append(accessModes, corev1.PersistentVolumeAccessMode(accessModeStr)) + } + + spec := corev1.PersistentVolumeClaimSpec{ + AccessModes: accessModes, + DataSource: &corev1.TypedLocalObjectReference{ + APIGroup: ptr.To(vs.GroupVersionKind().Group), + Kind: vs.Kind, + Name: vi.Spec.DataSource.ObjectRef.Name, + }, + } + + if storageClassName != "" { + spec.StorageClassName = &storageClassName + vi.Status.StorageClassName = storageClassName + } + + if volumeMode != "" { + spec.VolumeMode = ptr.To(corev1.PersistentVolumeMode(volumeMode)) + } + + if vs.Status != nil && vs.Status.RestoreSize != nil { + spec.Resources = corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: *vs.Status.RestoreSize, + }, + } + } + + pvcKey := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID).PersistentVolumeClaim() + + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: pvcKey.Name, + Namespace: pvcKey.Namespace, + OwnerReferences: []metav1.OwnerReference{ + service.MakeOwnerReference(vi), + }, + Finalizers: []string{ + virtv2.FinalizerVIProtection, + }, + }, + Spec: spec, + } +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_cr_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_cr_step.go new file mode 100644 index 0000000000..d0dbe9a6fe --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_cr_step.go @@ -0,0 +1,151 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package step + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + podutil "github.com/deckhouse/virtualization-controller/pkg/common/pod" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type ReadyContainerRegistryStepImporter interface { + CleanUpSupplements(ctx context.Context, sup *supplements.Generator) (bool, error) +} + +type ReadyContainerRegistryStepStat interface { + GetSize(pod *corev1.Pod) virtv2.ImageStatusSize + GetDVCRImageName(pod *corev1.Pod) string + GetFormat(pod *corev1.Pod) string + CheckPod(pod *corev1.Pod) error + GetCDROM(pod *corev1.Pod) bool +} + +type ReadyContainerRegistryStep struct { + pod *corev1.Pod + importer ReadyContainerRegistryStepImporter + stat ReadyContainerRegistryStepStat + recorder eventrecord.EventRecorderLogger + cb *conditions.ConditionBuilder +} + +func NewReadyContainerRegistryStep( + pod *corev1.Pod, + importer ReadyContainerRegistryStepImporter, + stat ReadyContainerRegistryStepStat, + recorder eventrecord.EventRecorderLogger, + cb *conditions.ConditionBuilder, +) *ReadyContainerRegistryStep { + return &ReadyContainerRegistryStep{ + pod: pod, + importer: importer, + stat: stat, + recorder: recorder, + cb: cb, + } +} + +func (s ReadyContainerRegistryStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + log, _ := logger.GetDataSourceContext(ctx, "objectref") + + ready, _ := conditions.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + if ready.Status == metav1.ConditionTrue { + log.Debug("Image is Ready") + + vi.Status.Phase = virtv2.ImageReady + s.cb. + Status(metav1.ConditionTrue). + Reason(vicondition.Ready). + Message("") + + return &reconcile.Result{}, nil + } + + if !podutil.IsPodComplete(s.pod) { + return nil, nil + } + + err := s.stat.CheckPod(s.pod) + if err != nil { + vi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrProvisioningFailed): + log.Debug("Provisioning is failed") + + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(err.Error() + ".")) + return &reconcile.Result{}, nil + default: + return nil, err + } + } + + err = s.cleanUpSupplements(ctx, vi) + if err != nil { + return nil, fmt.Errorf("clean up supplements: %w", err) + } + + log.Info("Image is Ready now") + + s.recorder.Event( + vi, + corev1.EventTypeNormal, + virtv2.ReasonDataSourceSyncCompleted, + "The ObjectRef DataSource import has completed", + ) + + s.cb. + Status(metav1.ConditionTrue). + Reason(vicondition.Ready). + Message("") + + vi.Status.Phase = virtv2.ImageReady + vi.Status.Size = s.stat.GetSize(s.pod) + vi.Status.CDROM = s.stat.GetCDROM(s.pod) + vi.Status.Format = s.stat.GetFormat(s.pod) + vi.Status.Progress = "100%" + vi.Status.Target.RegistryURL = s.stat.GetDVCRImageName(s.pod) + + return &reconcile.Result{}, nil +} + +func (s ReadyContainerRegistryStep) cleanUpSupplements(ctx context.Context, vi *virtv2.VirtualImage) error { + supgen := supplements.NewGenerator(annotations.VIShortName, vi.Name, vi.Namespace, vi.UID) + + _, err := s.importer.CleanUpSupplements(ctx, supgen) + if err != nil { + return err + } + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go new file mode 100644 index 0000000000..5c98a7d33d --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go @@ -0,0 +1,126 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package step + +import ( + "context" + "errors" + "fmt" + "strconv" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type ReadyPersistentVolumeClaimStep struct { + pvc *corev1.PersistentVolumeClaim + recorder eventrecord.EventRecorderLogger + cb *conditions.ConditionBuilder +} + +func NewReadyPersistentVolumeClaimStep( + pvc *corev1.PersistentVolumeClaim, + recorder eventrecord.EventRecorderLogger, + cb *conditions.ConditionBuilder, +) *ReadyPersistentVolumeClaimStep { + return &ReadyPersistentVolumeClaimStep{ + pvc: pvc, + recorder: recorder, + cb: cb, + } +} + +func (s ReadyPersistentVolumeClaimStep) Take(ctx context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + log, _ := logger.GetDataSourceContext(ctx, "objectref") + + if s.pvc == nil { + if vi.Status.Target.PersistentVolumeClaim != "" { + log.Warn("Image is Lost: underlying PVC not found") + + vi.Status.Phase = virtv2.ImageLost + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Lost). + Message(fmt.Sprintf("PersistentVolumeClaim %q not found.", vi.Status.Target.PersistentVolumeClaim)) + return &reconcile.Result{}, nil + } + + return nil, nil + } + + switch s.pvc.Status.Phase { + case corev1.ClaimLost: + log.Warn("Image is Lost: underlying PVC is Lost") + + vi.Status.Phase = virtv2.ImageLost + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Lost). + Message(fmt.Sprintf("PersistentVolume %q not found.", s.pvc.Spec.VolumeName)) + + return &reconcile.Result{}, nil + case corev1.ClaimBound: + log.Debug("Image is Ready") + + if vi.Status.Phase != virtv2.ImageReady { + s.recorder.Event( + vi, + corev1.EventTypeNormal, + virtv2.ReasonDataSourceSyncCompleted, + "The ObjectRef DataSource import has completed", + ) + } + + s.cb. + Status(metav1.ConditionTrue). + Reason(vdcondition.Ready). + Message("") + + vi.Status.Phase = virtv2.ImageReady + vi.Status.Progress = "100%" + + var res resource.Quantity + res = s.pvc.Status.Capacity[corev1.ResourceStorage] + + intQ, ok := res.AsInt64() + if !ok { + return nil, errors.New("failed to convert quantity to int64") + } + + vi.Status.Size = virtv2.ImageStatusSize{ + Stored: res.String(), + StoredBytes: strconv.FormatInt(intQ, 10), + Unpacked: res.String(), + UnpackedBytes: strconv.FormatInt(intQ, 10), + } + + vi.Status.Target.PersistentVolumeClaim = s.pvc.Name + + return &reconcile.Result{}, nil + default: + return nil, nil + } +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/terminating_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/terminating_step.go new file mode 100644 index 0000000000..6d4efbb907 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/terminating_step.go @@ -0,0 +1,52 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package step + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type TerminatingStep struct { + pvc *corev1.PersistentVolumeClaim +} + +func NewTerminatingStep(pvc *corev1.PersistentVolumeClaim) *TerminatingStep { + return &TerminatingStep{ + pvc: pvc, + } +} + +func (s TerminatingStep) Take(ctx context.Context, _ *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.pvc == nil { + return nil, nil + } + + if object.IsTerminating(s.pvc) { + log, _ := logger.GetDataSourceContext(ctx, "objectref") + log.Info("The PVC is terminating during an unfinished import process.") + return &reconcile.Result{Requeue: true}, nil + } + + return nil, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pod_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pod_step.go new file mode 100644 index 0000000000..432d2a2ff6 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pod_step.go @@ -0,0 +1,133 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package step + +import ( + "context" + "errors" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type WaitForPodStepStat interface { + GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + GetDVCRImageName(pod *corev1.Pod) string + CheckPod(pod *corev1.Pod) error +} + +type WaitForPodStep struct { + pod *corev1.Pod + pvc *corev1.PersistentVolumeClaim + stat WaitForPodStepStat + cb *conditions.ConditionBuilder +} + +func NewWaitForPodStep( + pod *corev1.Pod, + pvc *corev1.PersistentVolumeClaim, + stat WaitForPodStepStat, + cb *conditions.ConditionBuilder, +) *WaitForPodStep { + return &WaitForPodStep{ + pod: pod, + pvc: pvc, + stat: stat, + cb: cb, + } +} + +func (s WaitForPodStep) Take(_ context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.pod == nil { + vi.Status.Phase = virtv2.ImageProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Waiting for the importer pod to be created by controller.") + + return &reconcile.Result{}, nil + } + + err := s.stat.CheckPod(s.pod) + if err != nil { + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + if strings.Contains(err.Error(), "pod has unbound immediate PersistentVolumeClaims") { + vi.Status.Phase = virtv2.ImageProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Waiting for PersistentVolumeClaim to be Bound") + + return &reconcile.Result{Requeue: true}, nil + } + + vi.Status.Phase = virtv2.ImageFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningNotStarted). + Message(service.CapitalizeFirstLetter(err.Error() + ".")) + return &reconcile.Result{}, nil + case errors.Is(err, service.ErrProvisioningFailed): + vi.Status.Phase = virtv2.ImageFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(err.Error() + ".")) + return &reconcile.Result{}, nil + default: + vi.Status.Phase = virtv2.ImageFailed + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.ProvisioningFailed). + Message(service.CapitalizeFirstLetter(err.Error() + ".")) + return &reconcile.Result{}, err + } + } + + if s.pod.Status.Phase != corev1.PodRunning { + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Preparing to start import to DVCR.") + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Target.RegistryURL = s.stat.GetDVCRImageName(s.pod) + + return &reconcile.Result{}, nil + } + + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Import is in the process of provisioning to DVCR.") + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Progress = s.stat.GetProgress(vi.GetUID(), s.pod, vi.Status.Progress) + vi.Status.Target.RegistryURL = s.stat.GetDVCRImageName(s.pod) + + return &reconcile.Result{RequeueAfter: 2 * time.Second}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pvc_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pvc_step.go new file mode 100644 index 0000000000..83702504a4 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/wait_for_pvc_step.go @@ -0,0 +1,70 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package step + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type WaitForPVCStep struct { + pvc *corev1.PersistentVolumeClaim + cb *conditions.ConditionBuilder +} + +func NewWaitForPVCStep( + pvc *corev1.PersistentVolumeClaim, + cb *conditions.ConditionBuilder, +) *WaitForPVCStep { + return &WaitForPVCStep{ + pvc: pvc, + cb: cb, + } +} + +func (s WaitForPVCStep) Take(_ context.Context, vi *virtv2.VirtualImage) (*reconcile.Result, error) { + if s.pvc == nil { + vi.Status.Phase = virtv2.ImageProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vicondition.Provisioning). + Message("Waiting for the underlying PersistentVolumeClaim to be created by controller.") + + return &reconcile.Result{}, nil + } + + if s.pvc.Status.Phase == corev1.ClaimBound { + return nil, nil + } + + vi.Status.Phase = virtv2.ImageProvisioning + s.cb. + Status(metav1.ConditionFalse). + Reason(vdcondition.Provisioning). + Message(fmt.Sprintf("Waiting for the PVC %s to be Bound.", s.pvc.Name)) + + return &reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/pod_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/pod_watcher.go new file mode 100644 index 0000000000..b34e0e8f83 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/pod_watcher.go @@ -0,0 +1,77 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package watcher + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/deckhouse/pkg/log" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type PodWatcher struct { + logger *log.Logger + client client.Client +} + +func NewPodWatcher(client client.Client) *PodWatcher { + return &PodWatcher{ + logger: log.Default().With("watcher", "pod"), + client: client, + } +} + +func (w PodWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &corev1.Pod{}), + handler.EnqueueRequestForOwner( + mgr.GetScheme(), + mgr.GetRESTMapper(), + &virtv2.VirtualImage{}, + ), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return true }, + DeleteFunc: func(e event.DeleteEvent) bool { return false }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w PodWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldPod, ok := e.ObjectOld.(*corev1.Pod) + if !ok { + w.logger.Error(fmt.Sprintf("expected an old Pod but got a %T", e.ObjectOld)) + return false + } + + newPod, ok := e.ObjectNew.(*corev1.Pod) + if !ok { + w.logger.Error(fmt.Sprintf("expected a new Pod but got a %T", e.ObjectNew)) + return false + } + + return oldPod.Status.Phase != newPod.Status.Phase +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vdsnapshot_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vdsnapshot_watcher.go new file mode 100644 index 0000000000..a6716203ca --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vdsnapshot_watcher.go @@ -0,0 +1,124 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package watcher + +import ( + "context" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/controller/indexer" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualDiskSnapshotWatcher struct { + logger *log.Logger + client client.Client +} + +func NewVirtualDiskSnapshotWatcher(client client.Client) *VirtualDiskSnapshotWatcher { + return &VirtualDiskSnapshotWatcher{ + logger: log.Default().With("watcher", strings.ToLower(virtv2.VirtualDiskSnapshotKind)), + client: client, + } +} + +func (w VirtualDiskSnapshotWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualDiskSnapshot{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return true }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualDiskSnapshotWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + vdSnapshot, ok := obj.(*virtv2.VirtualDiskSnapshot) + if !ok { + w.logger.Error(fmt.Sprintf("expected a VirtualDiskSnapshot but got a %T", obj)) + return + } + + var vis virtv2.VirtualImageList + err := w.client.List(ctx, &vis, &client.ListOptions{ + Namespace: vdSnapshot.Namespace, + FieldSelector: fields.OneTermEqualSelector(indexer.IndexFieldVIByVDSnapshot, vdSnapshot.Name), + }) + if err != nil { + w.logger.Error(fmt.Sprintf("failed to list virtual images: %s", err)) + return + } + + for _, vi := range vis.Items { + if !isSnapshotDataSource(vi.Spec.DataSource, vdSnapshot.Name) { + w.logger.Error("vd list by vd snapshot returns unexpected resources, please report a bug") + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vi.Name, + Namespace: vi.Namespace, + }, + }) + } + + return +} + +func (w VirtualDiskSnapshotWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVDSnapshot, ok := e.ObjectOld.(*virtv2.VirtualDiskSnapshot) + if !ok { + w.logger.Error(fmt.Sprintf("expected an old VirtualDiskSnapshot but got a %T", e.ObjectOld)) + return false + } + + newVDSnapshot, ok := e.ObjectNew.(*virtv2.VirtualDiskSnapshot) + if !ok { + w.logger.Error(fmt.Sprintf("expected a new VirtualDiskSnapshot but got a %T", e.ObjectNew)) + return false + } + + return oldVDSnapshot.Status.Phase != newVDSnapshot.Status.Phase +} + +func isSnapshotDataSource(ds virtv2.VirtualImageDataSource, vdSnapshotName string) bool { + if ds.Type != virtv2.DataSourceTypeObjectRef { + return false + } + + if ds.ObjectRef == nil || ds.ObjectRef.Kind != virtv2.VirtualImageObjectRefKindVirtualDiskSnapshot { + return false + } + + return ds.ObjectRef.Name == vdSnapshotName +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vm_watcher.go b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vm_watcher.go new file mode 100644 index 0000000000..3073e55542 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/watcher/vm_watcher.go @@ -0,0 +1,125 @@ +/* +Copyright 2025 Flant JSC + +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. +*/ + +package watcher + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineWatcher struct { + logger *log.Logger + client client.Client +} + +func NewVirtualMachineWatcher(client client.Client) *VirtualMachineWatcher { + return &VirtualMachineWatcher{ + logger: log.Default().With("watcher", "vm"), + client: client, + } +} + +func (w VirtualMachineWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualMachine{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return w.hasVirtualImageRef(e.Object) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return w.hasVirtualImageRef(e.Object) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return w.hasVirtualImageRef(e.ObjectOld) || w.hasVirtualImageRef(e.ObjectNew) + }, + }, + ) +} + +func (w VirtualMachineWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + vm, ok := obj.(*virtv2.VirtualMachine) + if !ok { + w.logger.Error(fmt.Sprintf("expected a VirtualMachine but got a %T", obj)) + return + } + + for _, ref := range vm.Status.BlockDeviceRefs { + if ref.Kind != virtv2.ImageDevice { + continue + } + + vi, err := object.FetchObject(ctx, types.NamespacedName{ + Namespace: vm.Namespace, + Name: ref.Name, + }, w.client, &virtv2.VirtualImage{}) + if err != nil { + w.logger.Error("Failed to fetch vi to reconcile", logger.SlogErr(err)) + continue + } + + if vi == nil { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vi.Name, + Namespace: vi.Namespace, + }, + }) + } + + return +} + +func (w VirtualMachineWatcher) hasVirtualImageRef(obj client.Object) bool { + vm, ok := obj.(*virtv2.VirtualMachine) + if !ok { + w.logger.Error(fmt.Sprintf("expected a VirtualMachine but got a %T", obj)) + return false + } + + for _, ref := range vm.Spec.BlockDeviceRefs { + if ref.Kind == virtv2.ImageDevice { + return true + } + } + + for _, ref := range vm.Status.BlockDeviceRefs { + if ref.Kind == virtv2.ImageDevice { + return true + } + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go index c2a3ddb70a..db7d52ec90 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go @@ -44,6 +44,10 @@ import ( "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" ) +type Watcher interface { + Watch(mgr manager.Manager, ctr controller.Controller) error +} + type Handler interface { Handle(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) Name() string @@ -175,7 +179,7 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr mgr.GetRESTMapper(), &virtv2.VirtualImage{}, ), predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { return false }, + CreateFunc: func(e event.CreateEvent) bool { return true }, DeleteFunc: func(e event.DeleteEvent) bool { return true }, UpdateFunc: func(e event.UpdateEvent) bool { oldPVC, ok := e.ObjectOld.(*corev1.PersistentVolumeClaim) @@ -248,9 +252,16 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr return fmt.Errorf("error setting watch on CVIs: %w", err) } - storageClassReadyWatcher := watcher.NewStorageClassWatcher(mgr.GetClient()) - if err := storageClassReadyWatcher.Watch(mgr, ctr); err != nil { - return fmt.Errorf("error setting watch on StorageClass: %w", err) + for _, w := range []Watcher{ + watcher.NewPodWatcher(mgr.GetClient()), + watcher.NewStorageClassWatcher(mgr.GetClient()), + watcher.NewVirtualMachineWatcher(mgr.GetClient()), + watcher.NewVirtualDiskSnapshotWatcher(mgr.GetClient()), + } { + err := w.Watch(mgr, ctr) + if err != nil { + return fmt.Errorf("error setting watcher: %w", err) + } } return nil diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go index 75c9a93fd2..375cf4598d 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go @@ -18,7 +18,6 @@ package vi import ( "context" - "errors" "fmt" "reflect" "strings" @@ -28,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/common/blockdevice" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" @@ -49,8 +49,12 @@ func (v *Validator) ValidateCreate(_ context.Context, obj runtime.Object) (admis return nil, fmt.Errorf("expected a new VirtualMachine but got a %T", obj) } - if strings.Contains(vi.ObjectMeta.Name, ".") { - return nil, errors.New("VirtualImage name is invalid: '.' is forbidden, allowed name symbols are [0-9a-zA-Z-]") + if strings.Contains(vi.Name, ".") { + return nil, fmt.Errorf("the VirtualImage name %q is invalid: '.' is forbidden, allowed name symbols are [0-9a-zA-Z-]", vi.Name) + } + + if len(vi.Name) > blockdevice.MaxVirtualImageNameLen { + return nil, fmt.Errorf("the VirtualImage name %q is too long: it must be no more than %d characters", vi.Name, blockdevice.MaxVirtualImageNameLen) } if vi.Spec.Storage == virtv2.StorageKubernetes { @@ -94,8 +98,12 @@ func (v *Validator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Obj } } - if strings.Contains(newVI.ObjectMeta.Name, ".") { - warnings = append(warnings, "VirtualImage name is invalid as it contains now forbidden symbol '.', allowed symbols for name are [0-9a-zA-Z-]. Create another image with valid name to avoid problems with future updates.") + if strings.Contains(newVI.Name, ".") { + warnings = append(warnings, fmt.Sprintf(" the VirtualImage name %q is invalid as it contains now forbidden symbol '.', allowed symbols for name are [0-9a-zA-Z-]. Create another image with valid name to avoid problems with future updates.", newVI.Name)) + } + + if len(newVI.Name) > blockdevice.MaxVirtualImageNameLen { + warnings = append(warnings, fmt.Sprintf("the VirtualImage name %q is too long: it must be no more than %d characters", newVI.Name, blockdevice.MaxVirtualImageNameLen)) } return warnings, nil diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go index ee3672dc4c..a1aeafb149 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/lifecycle.go @@ -92,11 +92,7 @@ func (h *LifeCycleHandler) Handle(ctx context.Context, s state.VirtualMachineSta return reconcile.Result{}, err } - phase := getPhase(kvvm) - if phase == "" { - phase = virtv2.MachinePending - } - changed.Status.Phase = phase + changed.Status.Phase = getPhase(changed, kvvm) kvvmi, err := s.KVVMI(ctx) if err != nil { diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go index 6d0ab591f9..4e45822f17 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state.go @@ -22,16 +22,20 @@ import ( corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" kvvmutil "github.com/deckhouse/virtualization-controller/pkg/common/kvvm" + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/powerstate" "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" "github.com/deckhouse/virtualization-controller/pkg/eventrecord" "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" ) const nameSyncPowerStateHandler = "SyncPowerStateHandler" @@ -72,7 +76,12 @@ func (h *SyncPowerStateHandler) Handle(ctx context.Context, s state.VirtualMachi } // syncPowerState enforces runPolicy on the underlying KVVM. -func (h *SyncPowerStateHandler) syncPowerState(ctx context.Context, s state.VirtualMachineState, kvvm *virtv1.VirtualMachine, runPolicy virtv2.RunPolicy) error { +func (h *SyncPowerStateHandler) syncPowerState( + ctx context.Context, + s state.VirtualMachineState, + kvvm *virtv1.VirtualMachine, + runPolicy virtv2.RunPolicy, +) error { if kvvm == nil { return nil } @@ -104,154 +113,282 @@ func (h *SyncPowerStateHandler) syncPowerState(ctx context.Context, s state.Virt shutdownInfo = s.ShutdownInfo }) + appliedCondition, _ := conditions.GetCondition(vmcondition.TypeConfigurationApplied, s.VirtualMachine().Changed().Status.Conditions) + isConfigurationApplied := appliedCondition.Status == metav1.ConditionTrue + switch runPolicy { case virtv2.AlwaysOffPolicy: - if kvvmi != nil { - h.recordStopEventf(ctx, s.VirtualMachine().Current(), - "Stop initiated by controller to ensure %s policy", - runPolicy, - ) + return h.handleAlwaysOffPolicy(ctx, s, kvvmi, runPolicy) + case virtv2.AlwaysOnPolicy: + return h.handleAlwaysOnPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, runPolicy, shutdownInfo) + case virtv2.AlwaysOnUnlessStoppedManually: + return h.handleAlwaysOnUnlessStoppedManuallyPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, runPolicy, shutdownInfo) + case virtv2.ManualPolicy: + return h.handleManualPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, runPolicy, shutdownInfo) + } - // Ensure KVVMI is absent. - err = h.client.Delete(ctx, kvvmi) - if err != nil && !k8serrors.IsNotFound(err) { - return fmt.Errorf("automatic stop VM for %s policy: delete KVVMI: %w", runPolicy, err) - } + return nil +} + +func (h *SyncPowerStateHandler) handleAlwaysOffPolicy( + ctx context.Context, + s state.VirtualMachineState, + kvvmi *virtv1.VirtualMachineInstance, + runPolicy virtv2.RunPolicy, +) error { + if kvvmi != nil { + h.recordStopEventf(ctx, s.VirtualMachine().Current(), + "Stop initiated by controller to ensure %s policy", + runPolicy, + ) + + err := h.client.Delete(ctx, kvvmi) + if err != nil && !k8serrors.IsNotFound(err) { + return fmt.Errorf("automatic stop VM for %s policy: delete KVVMI: %w", runPolicy, err) } - case virtv2.AlwaysOnPolicy: - if kvvmi == nil { - h.recordStartEventf(ctx, s.VirtualMachine().Current(), - "Start initiated by controller for %v policy", - runPolicy, - ) + } - if err = powerstate.StartVM(ctx, h.client, kvvm); err != nil { - return fmt.Errorf("failed to start VM: %w", err) - } + return nil +} + +func (h *SyncPowerStateHandler) handleManualPolicy( + ctx context.Context, + s state.VirtualMachineState, + kvvm *virtv1.VirtualMachine, + kvvmi *virtv1.VirtualMachineInstance, + isConfigurationApplied bool, + runPolicy virtv2.RunPolicy, + shutdownInfo powerstate.ShutdownInfo, +) error { + if kvvmi == nil || kvvmi.DeletionTimestamp != nil { + return h.checkNeedStartVM(ctx, s, kvvm, isConfigurationApplied, runPolicy) + } + + if kvvm.Annotations[annotations.AnnVmRestartRequested] == "true" && kvvmi.Status.Phase == virtv1.Running { + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by VirtualMachineOparation for %s runPolicy", runPolicy) + err := h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) + if err != nil { + return err } - if kvvmi != nil && kvvmi.DeletionTimestamp == nil { - if kvvmi.Status.Phase == virtv1.Succeeded { - if shutdownInfo.PodCompleted { - // Treat completed Pod as restart if guest was restarted or as a start if guest was stopped. - switch shutdownInfo.Reason { - case powerstate.GuestResetReason: - h.recordRestartEventf(ctx, s.VirtualMachine().Current(), - "Restart initiated from inside the guest VM", - ) - default: - h.recordStartEventf(ctx, s.VirtualMachine().Current(), - "Start initiated by controller after stopping from inside the guest VM", - ) - } - } else { - h.recordRestartEventf(ctx, s.VirtualMachine().Current(), - "Restart initiated by controller for %v runPolicy", - runPolicy, - ) - } - err = powerstate.SafeRestartVM(ctx, h.client, kvvm, kvvmi) - if err != nil { - return fmt.Errorf("restart VM on guest-reset: %w", err) - } - } + err = kvvmutil.RemoveRestartAnnotation(ctx, h.client, kvvm) + if err != nil { + return err + } - if kvvmi.Status.Phase == virtv1.Failed { - h.recordRestartEventf(ctx, s.VirtualMachine().Current(), - "Restart initiated by controller for %s runPolicy after observing failed guest VM", - runPolicy, - ) - err = powerstate.SafeRestartVM(ctx, h.client, kvvm, kvvmi) - if err != nil { - return fmt.Errorf("restart VM on failed: %w", err) - } - } + return nil + } else if kvvmi.Status.Phase == virtv1.Succeeded && shutdownInfo.PodCompleted { + if shutdownInfo.Reason == powerstate.GuestResetReason { + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by inside the guest VirtualMachine for %s runPolicy", runPolicy) + return h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) + } else { + h.recordStopEventf(ctx, s.VirtualMachine().Current(), "Stop initiated from inside the guest VirtualMachine") + return h.deleteSucceededKVVMI(ctx, kvvmi) } - case virtv2.AlwaysOnUnlessStoppedManually: - if kvvmi != nil && kvvmi.DeletionTimestamp == nil { - if kvvmi.Status.Phase == virtv1.Succeeded { - if shutdownInfo.PodCompleted { - // Request to start new KVVMI if guest was restarted. - // Cleanup KVVMI is enough if VM was stopped from inside. - switch shutdownInfo.Reason { - case powerstate.GuestResetReason: - h.recordRestartEventf(ctx, s.VirtualMachine().Current(), - "Restart initiated from inside the guest VM", - ) - err = powerstate.SafeRestartVM(ctx, h.client, kvvm, kvvmi) - if err != nil { - return fmt.Errorf("restart VM on guest-reset: %w", err) - } - default: - vmPod, err := s.Pod(ctx) - if err != nil { - return fmt.Errorf("get virtual machine pod: %w", err) - } - - if vmPod != nil && !vmPod.GetObjectMeta().GetDeletionTimestamp().IsZero() { - h.recordRestartEventf(ctx, s.VirtualMachine().Current(), - "Restart initiated by controller for %s runPolicy after the deletion of pod VM.", - runPolicy, - ) - err = powerstate.SafeRestartVM(ctx, h.client, kvvm, kvvmi) - if err != nil { - return fmt.Errorf("automatic restart of failed VM: %w", err) - } - } else { - h.recordStopEventf(ctx, s.VirtualMachine().Current(), - "Stop initiated from inside the guest VM", - ) - err = h.client.Delete(ctx, kvvmi) - if err != nil && !k8serrors.IsNotFound(err) { - return fmt.Errorf("delete Succeeded KVVMI: %w", err) - } - } - } - } - } - if kvvmi.Status.Phase == virtv1.Failed { - h.recordRestartEventf(ctx, s.VirtualMachine().Current(), - "Restart initiated by controller for %s runPolicy after observing failed guest VM", - runPolicy, - ) - err = powerstate.SafeRestartVM(ctx, h.client, kvvm, kvvmi) - if err != nil { - return fmt.Errorf("automatic restart of failed VM: %w", err) - } + } + + return nil +} + +func (h *SyncPowerStateHandler) handleAlwaysOnPolicy( + ctx context.Context, + s state.VirtualMachineState, + kvvm *virtv1.VirtualMachine, + kvvmi *virtv1.VirtualMachineInstance, + isConfigurationApplied bool, + runPolicy virtv2.RunPolicy, + shutdownInfo powerstate.ShutdownInfo, +) error { + if kvvmi == nil { + h.recordStartEventf(ctx, s.VirtualMachine().Current(), "Start initiated by controller for %v policy", runPolicy) + return h.start(ctx, s, kvvm, isConfigurationApplied) + } + + if kvvmi.DeletionTimestamp != nil { + return h.checkNeedStartVM(ctx, s, kvvm, isConfigurationApplied, runPolicy) + } + + if kvvm.Annotations[annotations.AnnVmRestartRequested] == "true" && kvvmi.Status.Phase == virtv1.Running { + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by VirtualMachineOparation for %s runPolicy", runPolicy) + err := h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) + if err != nil { + return err + } + + err = kvvmutil.RemoveRestartAnnotation(ctx, h.client, kvvm) + if err != nil { + return err + } + + return nil + } + + if kvvmi.Status.Phase == virtv1.Succeeded || kvvmi.Status.Phase == virtv1.Failed { + if shutdownInfo.PodCompleted { + if shutdownInfo.Reason == powerstate.GuestResetReason { + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by inside the guest VirtualMachine for %s runPolicy", runPolicy) + return h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) } } - case virtv2.ManualPolicy: - // Manual policy requires to handle only guest-reset event. - // All types of shutdown are final states. - if kvvmi != nil && kvvmi.DeletionTimestamp == nil { - if kvvmi.Status.Phase == virtv1.Succeeded && shutdownInfo.PodCompleted { - // Request to start new KVVMI (with updated settings). - switch shutdownInfo.Reason { - case powerstate.GuestResetReason: - h.recordRestartEventf(ctx, s.VirtualMachine().Current(), - "Restart initiated from inside the guest VM", - ) - err = powerstate.SafeRestartVM(ctx, h.client, kvvm, kvvmi) - if err != nil { - return fmt.Errorf("restart VM on guest-reset: %w", err) - } - default: - h.recordStopEventf(ctx, s.VirtualMachine().Current(), - "Stop initiated from inside the guest VM", - ) - // Cleanup old version of KVVMI. - err = h.client.Delete(ctx, kvvmi) - if err != nil && !k8serrors.IsNotFound(err) { - return fmt.Errorf("delete Succeeded KVVMI: %w", err) - } + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by controller after stopping from inside the guest VirtualMachine for %s runPolicy", runPolicy) + return h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) + } + + return nil +} + +func (h *SyncPowerStateHandler) handleAlwaysOnUnlessStoppedManuallyPolicy( + ctx context.Context, + s state.VirtualMachineState, + kvvm *virtv1.VirtualMachine, + kvvmi *virtv1.VirtualMachineInstance, + isConfigurationApplied bool, + runPolicy virtv2.RunPolicy, + shutdownInfo powerstate.ShutdownInfo, +) error { + if kvvmi == nil || kvvmi.DeletionTimestamp != nil { + return h.checkNeedStartVM(ctx, s, kvvm, isConfigurationApplied, runPolicy) + } + + if kvvm.Annotations[annotations.AnnVmRestartRequested] == "true" && kvvmi.Status.Phase == virtv1.Running { + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by VirtualMachineOparation for %s runPolicy", runPolicy) + err := h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) + if err != nil { + return err + } + + err = kvvmutil.RemoveRestartAnnotation(ctx, h.client, kvvm) + if err != nil { + return err + } + + return nil + } + switch kvvmi.Status.Phase { + case virtv1.Succeeded: + vmPod, err := s.Pod(ctx) + if err != nil { + return fmt.Errorf("get virtual machine pod: %w", err) + } + + if shutdownInfo.PodCompleted { + if shutdownInfo.Reason == powerstate.GuestResetReason { + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by inside the guest VirtualMachine for %s runPolicy", runPolicy) + return h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) + } else { + if vmPod == nil || !vmPod.GetObjectMeta().GetDeletionTimestamp().IsZero() { + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by controller after the deletion of pod VirtualMachine for %s runPolicy", runPolicy) + return h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) } + h.recordStopEventf(ctx, s.VirtualMachine().Current(), "Stop initiated from inside the guest VirtualMachine") + return h.deleteSucceededKVVMI(ctx, kvvmi) } } + + if vmPod == nil { + log, _ := logger.GetHandlerContext(ctx, nameSyncPowerStateHandler) + log.Error("failed to find VM pod") + } + case virtv1.Failed: + h.recordRestartEventf(ctx, s.VirtualMachine().Current(), "Restart initiated by controller after observing failed guest VirtualMachine for %s runPolicy", runPolicy) + return h.restart(ctx, s, kvvm, kvvmi, isConfigurationApplied) + default: + return nil } return nil } +func (h *SyncPowerStateHandler) checkNeedStartVM( + ctx context.Context, + s state.VirtualMachineState, + kvvm *virtv1.VirtualMachine, + isConfigurationApplied bool, + runPolicy virtv2.RunPolicy, +) error { + if isConfigurationApplied && (kvvm.Annotations[annotations.AnnVmStartRequested] == "true" || kvvm.Annotations[annotations.AnnVmRestartRequested] == "true") { + h.recordStartEventf(ctx, s.VirtualMachine().Current(), "Start initiated by controller for %v policy", runPolicy) + err := h.start(ctx, s, kvvm, isConfigurationApplied) + if err != nil { + return err + } + + err = kvvmutil.RemoveStartAnnotation(ctx, h.client, kvvm) + if err != nil { + return err + } + + err = kvvmutil.RemoveRestartAnnotation(ctx, h.client, kvvm) + if err != nil { + return err + } + } + + return nil +} + +func (h *SyncPowerStateHandler) start( + ctx context.Context, + s state.VirtualMachineState, + kvvm *virtv1.VirtualMachine, + isConfigurationApplied bool, +) error { + if !isConfigurationApplied { + h.recordStopEventf(ctx, s.VirtualMachine().Current(), + "The VirtualMachine has been interrupted because the provided configuration could not be applied.", + ) + return h.interruptRunningVM(ctx, kvvm, nil) + } + + if err := powerstate.StartVM(ctx, h.client, kvvm); err != nil { + return fmt.Errorf("failed to start VM: %w", err) + } + return nil +} + +func (h *SyncPowerStateHandler) restart( + ctx context.Context, + s state.VirtualMachineState, + kvvm *virtv1.VirtualMachine, + kvvmi *virtv1.VirtualMachineInstance, + isConfigurationApplied bool, +) error { + if !isConfigurationApplied { + h.recordStopEventf(ctx, s.VirtualMachine().Current(), + "The VirtualMachine has been interrupted because the provided configuration could not be applied.", + ) + return h.interruptRunningVM(ctx, kvvm, kvvmi) + } + + if err := powerstate.SafeRestartVM(ctx, h.client, kvvm, kvvmi); err != nil { + return fmt.Errorf("restart VM: %w", err) + } + return nil +} + +func (h *SyncPowerStateHandler) deleteSucceededKVVMI(ctx context.Context, kvvmi *virtv1.VirtualMachineInstance) error { + err := h.client.Delete(ctx, kvvmi) + if err != nil && !k8serrors.IsNotFound(err) { + return fmt.Errorf("delete Succeeded KVVMI: %w", err) + } + return nil +} + +func (h *SyncPowerStateHandler) interruptRunningVM(ctx context.Context, kvvm *virtv1.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance) error { + if kvvmi != nil { + err := h.client.Delete(ctx, kvvmi) + if err != nil && !k8serrors.IsNotFound(err) { + return fmt.Errorf("stop VM: delete KVVMI: %w", err) + } + } + + err := kvvmutil.AddStartAnnotation(ctx, h.client, kvvm) + if err != nil { + return fmt.Errorf("add annotation to KVVM: %w", err) + } + return nil +} + func (h *SyncPowerStateHandler) ensureRunStrategy(ctx context.Context, kvvm *virtv1.VirtualMachine, desiredRunStrategy virtv1.VirtualMachineRunStrategy) error { if kvvm == nil { return nil diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/util.go b/images/virtualization-artifact/pkg/controller/vm/internal/util.go index c62c169b07..1d9cf62abb 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/util.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/util.go @@ -26,6 +26,7 @@ import ( virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/service" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -66,69 +67,117 @@ func conditionStatus(status string) metav1.ConditionStatus { } func vmIsPending(kvvm *virtv1.VirtualMachine) bool { - return getPhase(kvvm) == virtv2.MachinePending + return getPhase(nil, kvvm) == virtv2.MachinePending } func vmIsStopped(kvvm *virtv1.VirtualMachine) bool { - return getPhase(kvvm) == virtv2.MachineStopped + return getPhase(nil, kvvm) == virtv2.MachineStopped } func vmIsCreated(kvvm *virtv1.VirtualMachine) bool { return kvvm != nil && kvvm.Status.Created } -func getPhase(kvvm *virtv1.VirtualMachine) virtv2.MachinePhase { +func getPhase(vm *virtv2.VirtualMachine, kvvm *virtv1.VirtualMachine) virtv2.MachinePhase { if kvvm == nil { return virtv2.MachinePending } - return mapPhases[kvvm.Status.PrintableStatus] + if handler, exists := mapPhases[kvvm.Status.PrintableStatus]; exists { + return handler(vm, kvvm) + } + + return virtv2.MachinePending } -var mapPhases = map[virtv1.VirtualMachinePrintableStatus]virtv2.MachinePhase{ +type PhaseGetter func(vm *virtv2.VirtualMachine, kvvm *virtv1.VirtualMachine) virtv2.MachinePhase + +var mapPhases = map[virtv1.VirtualMachinePrintableStatus]PhaseGetter{ // VirtualMachineStatusStopped indicates that the virtual machine is currently stopped and isn't expected to start. - virtv1.VirtualMachineStatusStopped: virtv2.MachineStopped, + virtv1.VirtualMachineStatusStopped: func(vm *virtv2.VirtualMachine, kvvm *virtv1.VirtualMachine) virtv2.MachinePhase { + if vm != nil && kvvm != nil { + confAppliedCondition, _ := conditions.GetCondition(vmcondition.TypeConfigurationApplied, vm.Status.Conditions) + if confAppliedCondition.Status == metav1.ConditionFalse && + kvvm != nil && kvvm.Annotations[annotations.AnnVmStartRequested] == "true" { + return virtv2.MachinePending + } + } + + return virtv2.MachineStopped + }, // VirtualMachineStatusProvisioning indicates that cluster resources associated with the virtual machine // (e.g., DataVolumes) are being provisioned and prepared. - virtv1.VirtualMachineStatusProvisioning: virtv2.MachineStarting, + virtv1.VirtualMachineStatusProvisioning: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachineStarting + }, // VirtualMachineStatusStarting indicates that the virtual machine is being prepared for running. - virtv1.VirtualMachineStatusStarting: virtv2.MachineStarting, + virtv1.VirtualMachineStatusStarting: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachineStarting + }, // VirtualMachineStatusRunning indicates that the virtual machine is running. - virtv1.VirtualMachineStatusRunning: virtv2.MachineRunning, + virtv1.VirtualMachineStatusRunning: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachineRunning + }, // VirtualMachineStatusPaused indicates that the virtual machine is paused. - virtv1.VirtualMachineStatusPaused: virtv2.MachinePause, + virtv1.VirtualMachineStatusPaused: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePause + }, // VirtualMachineStatusStopping indicates that the virtual machine is in the process of being stopped. - virtv1.VirtualMachineStatusStopping: virtv2.MachineStopping, + virtv1.VirtualMachineStatusStopping: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachineStopping + }, // VirtualMachineStatusTerminating indicates that the virtual machine is in the process of deletion, // as well as its associated resources (VirtualMachineInstance, DataVolumes, …). - virtv1.VirtualMachineStatusTerminating: virtv2.MachineTerminating, + virtv1.VirtualMachineStatusTerminating: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachineTerminating + }, // VirtualMachineStatusCrashLoopBackOff indicates that the virtual machine is currently in a crash loop waiting to be retried. - virtv1.VirtualMachineStatusCrashLoopBackOff: virtv2.MachinePending, + virtv1.VirtualMachineStatusCrashLoopBackOff: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, // VirtualMachineStatusMigrating indicates that the virtual machine is in the process of being migrated // to another host. - virtv1.VirtualMachineStatusMigrating: virtv2.MachineMigrating, + virtv1.VirtualMachineStatusMigrating: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachineMigrating + }, // VirtualMachineStatusUnknown indicates that the state of the virtual machine could not be obtained, // typically due to an error in communicating with the host on which it's running. - virtv1.VirtualMachineStatusUnknown: virtv2.MachinePending, + virtv1.VirtualMachineStatusUnknown: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, // VirtualMachineStatusUnschedulable indicates that an error has occurred while scheduling the virtual machine, // e.g. due to unsatisfiable resource requests or unsatisfiable scheduling constraints. - virtv1.VirtualMachineStatusUnschedulable: virtv2.MachinePending, + virtv1.VirtualMachineStatusUnschedulable: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, // VirtualMachineStatusErrImagePull indicates that an error has occurred while pulling an image for // a containerDisk VM volume. - virtv1.VirtualMachineStatusErrImagePull: virtv2.MachinePending, + virtv1.VirtualMachineStatusErrImagePull: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, // VirtualMachineStatusImagePullBackOff indicates that an error has occurred while pulling an image for // a containerDisk VM volume, and that kubelet is backing off before retrying. - virtv1.VirtualMachineStatusImagePullBackOff: virtv2.MachinePending, + virtv1.VirtualMachineStatusImagePullBackOff: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, // VirtualMachineStatusPvcNotFound indicates that the virtual machine references a PVC volume which doesn't exist. - virtv1.VirtualMachineStatusPvcNotFound: virtv2.MachinePending, + virtv1.VirtualMachineStatusPvcNotFound: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, // VirtualMachineStatusDataVolumeError indicates that an error has been reported by one of the DataVolumes // referenced by the virtual machines. - virtv1.VirtualMachineStatusDataVolumeError: virtv2.MachinePending, + virtv1.VirtualMachineStatusDataVolumeError: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, // VirtualMachineStatusWaitingForVolumeBinding indicates that some PersistentVolumeClaims backing // the virtual machine volume are still not bound. - virtv1.VirtualMachineStatusWaitingForVolumeBinding: virtv2.MachinePending, + virtv1.VirtualMachineStatusWaitingForVolumeBinding: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, - kvvmEmptyPhase: virtv2.MachinePending, + kvvmEmptyPhase: func(_ *virtv2.VirtualMachine, _ *virtv1.VirtualMachine) virtv2.MachinePhase { + return virtv2.MachinePending + }, } const ( diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go index ec32669eba..27ab236ab8 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" "github.com/deckhouse/virtualization-controller/pkg/controller/indexer" "github.com/deckhouse/virtualization-controller/pkg/controller/service" @@ -85,7 +86,9 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr oldVM := e.ObjectOld.(*virtv1.VirtualMachine) newVM := e.ObjectNew.(*virtv1.VirtualMachine) return oldVM.Status.PrintableStatus != newVM.Status.PrintableStatus || - oldVM.Status.Ready != newVM.Status.Ready + oldVM.Status.Ready != newVM.Status.Ready || + oldVM.Annotations[annotations.AnnVmStartRequested] != newVM.Annotations[annotations.AnnVmStartRequested] || + oldVM.Annotations[annotations.AnnVmRestartRequested] != newVM.Annotations[annotations.AnnVmRestartRequested] }, }, ); err != nil { diff --git a/images/virtualization-artifact/pkg/controller/vmop/internal/lifecycle.go b/images/virtualization-artifact/pkg/controller/vmop/internal/lifecycle.go index a9196a2b15..af7b8a4d1b 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/internal/lifecycle.go +++ b/images/virtualization-artifact/pkg/controller/vmop/internal/lifecycle.go @@ -116,7 +116,7 @@ func (h LifecycleHandler) Handle(ctx context.Context, s state.VMOperationState) if changed.Status.Phase == virtv2.VMOPPhaseInProgress { log.Debug("Operation in progress, check if VM is completed", "vm.phase", vm.Status.Phase, "vmop.phase", changed.Status.Phase) - return h.checkOperationComplete(ctx, changed, vm) + return h.syncOperationComplete(ctx, changed, vm) } // At this point VMOP is in Pending phase, do some validation checks. @@ -175,9 +175,9 @@ func (h LifecycleHandler) Name() string { return lifecycleHandlerName } -// checkOperationComplete detects if operation is completed and VM has desired phase. +// syncOperationComplete detects if operation is completed and VM has desired phase. // TODO detect if VM is stuck to prevent infinite InProgress state. -func (h LifecycleHandler) checkOperationComplete(ctx context.Context, changed *virtv2.VirtualMachineOperation, vm *virtv2.VirtualMachine) (reconcile.Result, error) { +func (h LifecycleHandler) syncOperationComplete(ctx context.Context, changed *virtv2.VirtualMachineOperation, vm *virtv2.VirtualMachine) (reconcile.Result, error) { completedCond := conditions.NewConditionBuilder(vmopcondition.TypeCompleted). Generation(changed.GetGeneration()) @@ -194,17 +194,29 @@ func (h LifecycleHandler) checkOperationComplete(ctx context.Context, changed *v completedCond. Reason(vmopcondition.ReasonOperationCompleted). Status(metav1.ConditionTrue). - Message("Virtual machine is in desired state"), + Message(""), &changed.Status.Conditions) return reconcile.Result{}, nil } // Keep InProgress phase as-is (InProgress), set complete condition to false. - conditions.SetCondition( - completedCond. - Reason(h.vmopSrv.InProgressReasonForType(changed)). - Status(metav1.ConditionFalse). - Message("Wait until operation completed"), - &changed.Status.Conditions) + if vm.Status.Phase == virtv2.MachinePending { + conditions.SetCondition( + completedCond. + Reason(h.vmopSrv.InProgressReasonForType(changed)). + Status(metav1.ConditionFalse). + Message("The request to restart the VirtualMachine has been sent. "+ + "The VirtualMachine is currently in the 'Pending' phase. "+ + "We are waiting for it to enter the 'Starting' phase."), + &changed.Status.Conditions) + } else { + conditions.SetCondition( + completedCond. + Reason(h.vmopSrv.InProgressReasonForType(changed)). + Status(metav1.ConditionFalse). + Message("Wait until operation completed"), + &changed.Status.Conditions) + } + return reconcile.Result{}, nil } diff --git a/images/virtualization-artifact/pkg/controller/vmop/internal/operation.go b/images/virtualization-artifact/pkg/controller/vmop/internal/operation.go index d07afde158..67e4e170f7 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/internal/operation.go +++ b/images/virtualization-artifact/pkg/controller/vmop/internal/operation.go @@ -114,7 +114,7 @@ func (h OperationHandler) Handle(ctx context.Context, s state.VMOperationState) msg := fmt.Sprintf("Sent signal %q to VM without errors.", changed.Spec.Type) log.Debug(msg) - h.recorder.Event(changed, corev1.EventTypeNormal, virtv2.ReasonVMOPSucceeded, msg) + h.recorder.Event(changed, corev1.EventTypeNormal, virtv2.ReasonVMOPInProgress, msg) changed.Status.Phase = virtv2.VMOPPhaseInProgress diff --git a/images/virtualization-artifact/pkg/controller/vmop/vmop_reconciler.go b/images/virtualization-artifact/pkg/controller/vmop/vmop_reconciler.go index b4d50b3349..2b0c85da1e 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/vmop_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vmop/vmop_reconciler.go @@ -88,7 +88,7 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr UpdateFunc: func(e event.UpdateEvent) bool { oldVM := e.ObjectOld.(*virtv2.VirtualMachine) newVM := e.ObjectNew.(*virtv2.VirtualMachine) - return oldVM.Status.Phase != newVM.Status.Phase + return oldVM.Status.Phase != newVM.Status.Phase || newVM.Status.MigrationState != nil }, }, ); err != nil { diff --git a/templates/kubevirt/virt-operator/rbac-for-us.yaml b/templates/kubevirt/virt-operator/rbac-for-us.yaml index 905f02d58c..d52cec41b0 100644 --- a/templates/kubevirt/virt-operator/rbac-for-us.yaml +++ b/templates/kubevirt/virt-operator/rbac-for-us.yaml @@ -226,6 +226,8 @@ rules: resources: - validatingwebhookconfigurations - mutatingwebhookconfigurations + - validatingadmissionpolicybindings + - validatingadmissionpolicies verbs: - get - list diff --git a/tests/e2e/testdata/images-creation/cvi/cvi_objectref_cvi.yaml b/tests/e2e/testdata/images-creation/cvi/cvi_objectref_cvi.yaml index 5b8802c728..9163c12e6b 100644 --- a/tests/e2e/testdata/images-creation/cvi/cvi_objectref_cvi.yaml +++ b/tests/e2e/testdata/images-creation/cvi/cvi_objectref_cvi.yaml @@ -1,7 +1,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-cvi + name: cvi-oref-cvi spec: dataSource: type: "ObjectRef" diff --git a/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vd.yaml b/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vd.yaml index a59c2e788f..64de1cf11d 100644 --- a/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vd.yaml +++ b/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vd.yaml @@ -1,7 +1,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vd + name: cvi-oref-vd spec: dataSource: type: "ObjectRef" diff --git a/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vdsnapshot.yaml b/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vdsnapshot.yaml index c46343bafd..9e6fbea226 100644 --- a/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vdsnapshot.yaml +++ b/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vdsnapshot.yaml @@ -1,7 +1,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vdsnapshot + name: cvi-oref-vdsnapshot spec: dataSource: type: "ObjectRef" diff --git a/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vi.yaml b/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vi.yaml index e87ec14577..e6e86738e5 100644 --- a/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vi.yaml +++ b/tests/e2e/testdata/images-creation/cvi/cvi_objectref_vi.yaml @@ -2,7 +2,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-http + name: cvi-oref-vi-http spec: dataSource: type: "ObjectRef" @@ -14,7 +14,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-containerimage + name: cvi-oref-vi-containerimage spec: dataSource: type: "ObjectRef" @@ -26,37 +26,37 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-objectref-cvi + name: cvi-oref-vi-oref-cvi spec: dataSource: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-cvi + name: vi-oref-cvi namespace: test-d8-virtualization --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-objectref-vd + name: cvi-oref-vi-oref-vd spec: dataSource: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-vd + name: vi-oref-vd namespace: test-d8-virtualization --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-objectref-vdsnapshot + name: cvi-oref-vi-oref-vdsnapshot spec: dataSource: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-vdsnapshot + name: vi-oref-vdsnapshot namespace: test-d8-virtualization # PVC SOURCE @@ -64,7 +64,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-http-pvc + name: cvi-oref-vi-http-pvc spec: dataSource: type: "ObjectRef" @@ -76,7 +76,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-containerimage-pvc + name: cvi-oref-vi-containerimage-pvc spec: dataSource: type: "ObjectRef" @@ -88,35 +88,35 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-objectref-cvi-pvc + name: cvi-oref-vi-oref-cvi-pvc spec: dataSource: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-pvc-objectref-cvi + name: vi-pvc-oref-cvi namespace: test-d8-virtualization --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-objectref-vd-pvc + name: cvi-oref-vi-oref-vd-pvc spec: dataSource: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-pvc-objectref-vd + name: vi-pvc-oref-vd namespace: test-d8-virtualization --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: ClusterVirtualImage metadata: - name: cvi-objectref-vi-objectref-vdsnapshot-pvc + name: cvi-oref-vi-oref-vdsnapshot-pvc spec: dataSource: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-pvc-objectref-vdsnapshot + name: vi-pvc-oref-vdsnapshot namespace: test-d8-virtualization diff --git a/tests/e2e/testdata/images-creation/vi/vi_objectref_cvi.yaml b/tests/e2e/testdata/images-creation/vi/vi_objectref_cvi.yaml index d749ae46c4..b8afcc6e96 100644 --- a/tests/e2e/testdata/images-creation/vi/vi_objectref_cvi.yaml +++ b/tests/e2e/testdata/images-creation/vi/vi_objectref_cvi.yaml @@ -1,7 +1,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-cvi + name: vi-oref-cvi namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -14,7 +14,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-cvi + name: vi-pvc-oref-cvi namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim diff --git a/tests/e2e/testdata/images-creation/vi/vi_objectref_vd.yaml b/tests/e2e/testdata/images-creation/vi/vi_objectref_vd.yaml index 477cdf1b0a..08aaebb211 100644 --- a/tests/e2e/testdata/images-creation/vi/vi_objectref_vd.yaml +++ b/tests/e2e/testdata/images-creation/vi/vi_objectref_vd.yaml @@ -1,7 +1,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vd + name: vi-oref-vd namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -14,7 +14,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vd + name: vi-pvc-oref-vd namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim diff --git a/tests/e2e/testdata/images-creation/vi/vi_objectref_vdsnapshot.yaml b/tests/e2e/testdata/images-creation/vi/vi_objectref_vdsnapshot.yaml index 637c4da284..465b45564a 100644 --- a/tests/e2e/testdata/images-creation/vi/vi_objectref_vdsnapshot.yaml +++ b/tests/e2e/testdata/images-creation/vi/vi_objectref_vdsnapshot.yaml @@ -1,7 +1,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vdsnapshot + name: vi-oref-vdsnapshot namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -14,7 +14,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vdsnapshot + name: vi-pvc-oref-vdsnapshot namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim diff --git a/tests/e2e/testdata/images-creation/vi/vi_objectref_vi.yaml b/tests/e2e/testdata/images-creation/vi/vi_objectref_vi.yaml index fb19ab3a73..7dd80ea06c 100644 --- a/tests/e2e/testdata/images-creation/vi/vi_objectref_vi.yaml +++ b/tests/e2e/testdata/images-creation/vi/vi_objectref_vi.yaml @@ -2,7 +2,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-http + name: vi-oref-vi-http namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -15,7 +15,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-containerimage + name: vi-oref-vi-containerimage namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -28,7 +28,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-objectref-cvi + name: vi-oref-vi-oref-cvi namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -36,12 +36,12 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-cvi + name: vi-oref-cvi --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-objectref-vd + name: vi-oref-vi-oref-vd namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -49,12 +49,12 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-vd + name: vi-oref-vd --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-objectref-vdsnapshot + name: vi-oref-vi-oref-vdsnapshot namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -62,14 +62,14 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-vdsnapshot + name: vi-oref-vdsnapshot # PVC SOURCE --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-http-pvc + name: vi-oref-vi-http-pvc namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -82,7 +82,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-containerimage-pvc + name: vi-oref-vi-containerimage-pvc namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -95,7 +95,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-objectref-cvi-pvc + name: vi-oref-vi-oref-cvi-pvc namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -103,12 +103,12 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-pvc-objectref-cvi + name: vi-pvc-oref-cvi --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-objectref-vd-pvc + name: vi-oref-vi-oref-vd-pvc namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -116,12 +116,12 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-pvc-objectref-vd + name: vi-pvc-oref-vd --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-objectref-vi-objectref-vdsnapshot-pvc + name: vi-oref-vi-oref-vdsnapshot-pvc namespace: test-d8-virtualization spec: storage: ContainerRegistry @@ -129,4 +129,4 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-pvc-objectref-vdsnapshot + name: vi-pvc-oref-vdsnapshot diff --git a/tests/e2e/testdata/images-creation/vi/vi_pvc_objectref_vi.yaml b/tests/e2e/testdata/images-creation/vi/vi_pvc_objectref_vi.yaml index 2765d60c7e..027ae375c0 100644 --- a/tests/e2e/testdata/images-creation/vi/vi_pvc_objectref_vi.yaml +++ b/tests/e2e/testdata/images-creation/vi/vi_pvc_objectref_vi.yaml @@ -2,7 +2,7 @@ apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-http + name: vi-pvc-oref-vi-http namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -15,7 +15,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-containerimage + name: vi-pvc-oref-vi-containerimage namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -28,7 +28,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-objectref-cvi + name: vi-pvc-oref-vi-oref-cvi namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -36,12 +36,12 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-cvi + name: vi-oref-cvi --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-objectref-vd + name: vi-pvc-oref-vi-oref-vd namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -49,12 +49,12 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-vd + name: vi-oref-vd --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-objectref-vdsnapshot + name: vi-pvc-oref-vi-oref-vdsnapshot namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -62,14 +62,14 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-objectref-vdsnapshot + name: vi-oref-vdsnapshot ### PVC --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-http-pvc + name: vi-pvc-oref-vi-http-pvc namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -82,7 +82,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-containerimage-pvc + name: vi-pvc-oref-vi-containerimage-pvc namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -95,7 +95,7 @@ spec: apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-objectref-vd-pvc + name: vi-pvc-oref-vi-oref-vd-pvc namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -103,12 +103,12 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-pvc-objectref-vd + name: vi-pvc-oref-vd --- apiVersion: virtualization.deckhouse.io/v1alpha2 kind: VirtualImage metadata: - name: vi-pvc-objectref-vi-objectref-vdsnapshot-pvc + name: vi-pvc-oref-vi-oref-vdsnapshot-pvc namespace: test-d8-virtualization spec: storage: PersistentVolumeClaim @@ -116,4 +116,4 @@ spec: type: "ObjectRef" objectRef: kind: "VirtualImage" - name: vi-pvc-objectref-vdsnapshot + name: vi-pvc-oref-vdsnapshot diff --git a/werf_cleanup.yaml b/werf_cleanup.yaml index 5f93167a06..db9cba6097 100644 --- a/werf_cleanup.yaml +++ b/werf_cleanup.yaml @@ -7,6 +7,6 @@ cleanup: limit: in: 168h # keep dev images build during last week which not main|pre-alpha - references: - branch: /main|pre-alpha/ + branch: /main|pre-alpha|release-.*/ imagesPerReference: last: 5 # keep 5 images for branches main|pre-alpha