From 2f489e4a1ae1a397e5a0ec00bc56e70b67f55ddc Mon Sep 17 00:00:00 2001 From: Isteb4k Date: Fri, 5 Jul 2024 13:21:52 +0200 Subject: [PATCH] feat(vmbda): apply new controller design * feat(vmbda): apply new design New vmbda controller design Conditions: BlockDeviceReady, VirtualMachineReady, Attached Signed-off-by: Isteb4k --- api/core/v1alpha2/events.go | 3 + .../virtual_machine_block_disk_attachment.go | 23 +- api/core/v1alpha2/vmbdacondition/condition.go | 43 ++ api/core/v1alpha2/zz_generated.deepcopy.go | 9 +- .../generated/openapi/zz_generated.openapi.go | 27 +- crds/doc-ru-virtualimage.yaml | 39 +- ...u-virtualmachineblockdeviceattachment.yaml | 29 +- crds/virtualdisk.yaml | 2 +- crds/virtualimage.yaml | 40 +- crds/virtualmachineblockdeviceattachment.yaml | 48 ++- .../cmd/virtualization-controller/main.go | 8 +- .../pkg/controller/kvbuilder/kvvm_utils.go | 9 + .../controller/service/attachment_service.go | 204 ++++++++++ .../pkg/controller/service/disk_service.go | 16 +- .../controller/service/importer_service.go | 15 +- .../pkg/controller/service/resource.go | 25 +- .../controller/service/uploader_service.go | 15 +- .../controller/vm/internal/block_device.go | 79 ++-- .../pkg/controller/vm/internal/state/state.go | 16 +- .../pkg/controller/vm/vm_reconciler.go | 22 +- .../vmbda/internal/block_device_ready.go | 130 ++++++ .../pkg/controller/vmbda/internal/deletion.go | 42 ++ .../controller/vmbda/internal/life_cycle.go | 209 ++++++++++ .../vmbda/internal/virtual_machine_ready.go | 121 ++++++ .../vmbda/internal/watcher/kvvmi_watcher.go | 157 ++++++++ .../vmbda/internal/watcher/vd_watcher.go | 106 +++++ .../vmbda/internal/watcher/vm_watcher.go | 106 +++++ .../vmbda/internal/watcher/vmbda_watcher.go | 57 +++ .../pkg/controller/vmbda/vmbda_controller.go | 92 +++++ .../pkg/controller/vmbda/vmbda_reconciler.go | 121 ++++++ .../pkg/controller/vmbda/vmbda_webhook.go | 70 ++++ .../pkg/controller/vmbda_controller.go | 88 ----- .../pkg/controller/vmbda_reconciler.go | 371 ------------------ .../pkg/controller/vmbda_reconciler_state.go | 176 --------- .../pkg/controller/vmbda_webhook.go | 96 ----- .../pkg/monitoring/metrics/vmbda/collector.go | 4 +- .../validation-webhook.yaml | 2 +- 37 files changed, 1736 insertions(+), 884 deletions(-) create mode 100644 api/core/v1alpha2/vmbdacondition/condition.go create mode 100644 images/virtualization-artifact/pkg/controller/service/attachment_service.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/block_device_ready.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/deletion.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/kvvmi_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vd_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vmbda_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/vmbda_controller.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/vmbda_reconciler.go create mode 100644 images/virtualization-artifact/pkg/controller/vmbda/vmbda_webhook.go delete mode 100644 images/virtualization-artifact/pkg/controller/vmbda_controller.go delete mode 100644 images/virtualization-artifact/pkg/controller/vmbda_reconciler.go delete mode 100644 images/virtualization-artifact/pkg/controller/vmbda_reconciler_state.go delete mode 100644 images/virtualization-artifact/pkg/controller/vmbda_webhook.go diff --git a/api/core/v1alpha2/events.go b/api/core/v1alpha2/events.go index ef926532e..6939e6a05 100644 --- a/api/core/v1alpha2/events.go +++ b/api/core/v1alpha2/events.go @@ -50,6 +50,9 @@ const ( // ReasonVMWaitForBlockDevices is event reason that block devices used by VM are not ready yet. ReasonVMWaitForBlockDevices = "WaitForBlockDevices" + // ReasonUnknownHotPluggedVolume is event reason that volume was hot plugged to VirtualMachineInstance, but it is not a VirtualDisk. + ReasonUnknownHotPluggedVolume = "UnknownHotPluggedVolume" + // ReasonVMChangesApplied is event reason that changes applied from VM to underlying KVVM. ReasonVMChangesApplied = "ChangesApplied" diff --git a/api/core/v1alpha2/virtual_machine_block_disk_attachment.go b/api/core/v1alpha2/virtual_machine_block_disk_attachment.go index 006a25caf..623f9532b 100644 --- a/api/core/v1alpha2/virtual_machine_block_disk_attachment.go +++ b/api/core/v1alpha2/virtual_machine_block_disk_attachment.go @@ -45,8 +45,8 @@ type VirtualMachineBlockDeviceAttachmentList struct { } type VirtualMachineBlockDeviceAttachmentSpec struct { - VirtualMachine string `json:"virtualMachineName"` - BlockDeviceRef VMBDAObjectRef `json:"blockDeviceRef"` + VirtualMachineName string `json:"virtualMachineName"` + BlockDeviceRef VMBDAObjectRef `json:"blockDeviceRef"` } type VMBDAObjectRef struct { @@ -60,21 +60,18 @@ const ( VMBDAObjectRefKindVirtualDisk VMBDAObjectRefKind = "VirtualDisk" ) -type VirtualMachineBlockDeviceAttachmentObjectRefKind string - -const BlockDeviceAttachmentTypeVirtualDisk VirtualMachineBlockDeviceAttachmentObjectRefKind = "VirtualDisk" - type VirtualMachineBlockDeviceAttachmentStatus struct { - VirtualMachine string `json:"virtualMachine,omitempty"` - Phase BlockDeviceAttachmentPhase `json:"phase,omitempty"` - FailureReason string `json:"failureReason,omitempty"` - FailureMessage string `json:"failureMessage,omitempty"` + Phase BlockDeviceAttachmentPhase `json:"phase,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } type BlockDeviceAttachmentPhase string const ( - BlockDeviceAttachmentPhaseInProgress BlockDeviceAttachmentPhase = "InProgress" - BlockDeviceAttachmentPhaseAttached BlockDeviceAttachmentPhase = "Attached" - BlockDeviceAttachmentPhaseFailed BlockDeviceAttachmentPhase = "Failed" + BlockDeviceAttachmentPhasePending BlockDeviceAttachmentPhase = "Pending" + BlockDeviceAttachmentPhaseInProgress BlockDeviceAttachmentPhase = "InProgress" + BlockDeviceAttachmentPhaseAttached BlockDeviceAttachmentPhase = "Attached" + BlockDeviceAttachmentPhaseFailed BlockDeviceAttachmentPhase = "Failed" + BlockDeviceAttachmentPhaseTerminating BlockDeviceAttachmentPhase = "Terminating" ) diff --git a/api/core/v1alpha2/vmbdacondition/condition.go b/api/core/v1alpha2/vmbdacondition/condition.go new file mode 100644 index 000000000..9e70bd738 --- /dev/null +++ b/api/core/v1alpha2/vmbdacondition/condition.go @@ -0,0 +1,43 @@ +/* +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 vmbdacondition + +type Type = string + +const ( + BlockDeviceReadyType Type = "BlockDeviceReady" + VirtualMachineReadyType Type = "VirtualMachineReady" + AttachedType Type = "Attached" +) + +type ( + BlockDeviceReadyReason = string + VirtualMachineReadyReason = string + AttachedReason = string +) + +const ( + BlockDeviceReady BlockDeviceReadyReason = "BlockDeviceReady" + BlockDeviceNotReady BlockDeviceReadyReason = "BlockDeviceNotReady" + + VirtualMachineReady VirtualMachineReadyReason = "VirtualMachineReady" + VirtualMachineNotReady VirtualMachineReadyReason = "VirtualMachineNotReady" + + Attached AttachedReason = "Attached" + NotAttached AttachedReason = "NotAttached" + AttachmentRequestSent AttachedReason = "AttachmentRequestSent" +) diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index 77ab9444c..388d31bd6 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -1064,7 +1064,7 @@ func (in *VirtualMachineBlockDeviceAttachment) DeepCopyInto(out *VirtualMachineB out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -1139,6 +1139,13 @@ func (in *VirtualMachineBlockDeviceAttachmentSpec) DeepCopy() *VirtualMachineBlo // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualMachineBlockDeviceAttachmentStatus) DeepCopyInto(out *VirtualMachineBlockDeviceAttachmentStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go index 8c887553c..00b18a775 100644 --- a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go +++ b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go @@ -2524,33 +2524,36 @@ func schema_virtualization_api_core_v1alpha2_VirtualMachineBlockDeviceAttachment SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "virtualMachine": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, "phase": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, Format: "", }, }, - "failureReason": { + "conditions": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"), + }, + }, + }, }, }, - "failureMessage": { + "observedGeneration": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Type: []string{"integer"}, + Format: "int64", }, }, }, }, }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, } } diff --git a/crds/doc-ru-virtualimage.yaml b/crds/doc-ru-virtualimage.yaml index 527292330..6c022ea68 100644 --- a/crds/doc-ru-virtualimage.yaml +++ b/crds/doc-ru-virtualimage.yaml @@ -91,20 +91,22 @@ spec: properties: conditions: description: | - Последние доступные наблюдения текущего состояния объекта. - properties: - type: - description: Тип состояния. - status: - description: Статус состояния (одно из True, False, Unknown). - message: - description: Cообщение c деталями последнего перехода состояния. - reason: - description: Краткая причина последнего перехода состояния. - lastProbeTime: - description: Время последней проверки состояния. - lastTransitionTime: - description: Время последнего перехода состояния из одного статуса в другой. + Последнее подтвержденное состояние данного ресурса. + items: + properties: + lastProbeTime: + description: Время проверки условия. + lastTransitionTime: + description: Время перехода условия из одного состояния в другое. + message: + description: Удобочитаемое сообщение с подробной информацией о последнем переходе. + reason: + description: Краткая причина последнего перехода состояния. + status: + description: | + Статус условия. Возможные значения: `True`, `False`, `Unknown`. + type: + description: Тип условия. cdrom: description: | Является ли образ форматом, который должен быть смонтирован как cdrom, например iso и т. д. @@ -124,12 +126,6 @@ spec: currentBytes: description: | Текущая скорость загрузки в байтах в секунду. - failureMessage: - description: | - Подробное описание ошибки. - failureReason: - description: | - Краткое описание причины ошибки. format: description: | Обнаруженный формат образа. @@ -170,3 +166,6 @@ spec: uploadCommand: description: | Команда для загрузки образа для типа 'Upload'. + observedGeneration: + description: | + Поколение ресурса, которое в последний раз обрабатывалось контроллером. diff --git a/crds/doc-ru-virtualmachineblockdeviceattachment.yaml b/crds/doc-ru-virtualmachineblockdeviceattachment.yaml index 7ef2b16aa..ded53f415 100644 --- a/crds/doc-ru-virtualmachineblockdeviceattachment.yaml +++ b/crds/doc-ru-virtualmachineblockdeviceattachment.yaml @@ -25,19 +25,36 @@ spec: Имя виртуальной машины, к которой подключен диск. status: properties: - failureMessage: + conditions: description: | - Подробное описание ошибки. - failureReason: - description: | - Краткое описание причины ошибки. + Последнее подтвержденное состояние данного ресурса. + items: + properties: + lastProbeTime: + description: Время проверки условия. + lastTransitionTime: + description: Время перехода условия из одного состояния в другое. + message: + description: Удобочитаемое сообщение с подробной информацией о последнем переходе. + reason: + description: Краткая причина последнего перехода состояния. + status: + description: | + Статус условия. Возможные значения: `True`, `False`, `Unknown`. + type: + description: Тип условия. phase: description: | Фаза ресурса: + * Pending — ресурс был создан и находится в очереди ожидания. * InProgress — диск в процессе подключения к ВМ. * Attached — диск подключен к ВМ. - * Failed — возникла проблема с подключением диска. Смотрите `.status.failureReason`. + * Failed — возникла проблема с подключением диска. + * Terminating - Ресурс находится в процессе удаления. virtualMachineName: description: | Имя виртуальной машины, к которой подключен этот диск. + observedGeneration: + description: | + Поколение ресурса, которое в последний раз обрабатывалось контроллером. diff --git a/crds/virtualdisk.yaml b/crds/virtualdisk.yaml index b4ae436a9..a57e31072 100644 --- a/crds/virtualdisk.yaml +++ b/crds/virtualdisk.yaml @@ -266,7 +266,7 @@ spec: * WaitForUserUpload - Waiting for the user to upload the image. The endpoint to upload the image is specified in `.status.uploadCommand`. * Ready - The resource is created and ready to use. * Resizing — The process of resource resizing is in progress. - * Failed - There was a problem when creating a resource, details can be seen in `.status.failureReason` and `.status.failureMessage`. + * Failed - There was a problem when creating a resource. * PVCLost - The child PVC of the resource is missing. The resource cannot be used. * Terminating - The process of resource deletion is in progress. enum: diff --git a/crds/virtualimage.yaml b/crds/virtualimage.yaml index a8c6ba643..2f827d4a0 100644 --- a/crds/virtualimage.yaml +++ b/crds/virtualimage.yaml @@ -193,40 +193,36 @@ spec: type: object properties: conditions: - type: array description: | The latest available observations of an object's current state. + type: array items: type: object properties: - type: - type: string - description: Type of condition. - status: - type: string - description: Status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - "Unknown" - reason: - type: string - description: Reason for the condition's last transition. - message: - type: string - description: | - Human readable message indicating details about last transition. lastProbeTime: - type: string description: Last time the condition was checked. format: date-time - lastTransitionTime: type: string + lastTransitionTime: description: Last time the condition transit from one status to another. format: date-time + type: string + message: + description: Human readable message indicating details about last transition. + type: string + reason: + description: (brief) reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + enum: ["True", "False", "Unknown"] + type: + description: Type of condition. + type: string required: - - type - status + - type downloadSpeed: type: object description: | @@ -325,7 +321,7 @@ spec: observedGeneration: type: integer description: | - Represents the .metadata.generation that the status was set based upon. + The generation last processed by the controller. additionalPrinterColumns: - name: Phase type: string diff --git a/crds/virtualmachineblockdeviceattachment.yaml b/crds/virtualmachineblockdeviceattachment.yaml index a14bc6138..88d2593bc 100644 --- a/crds/virtualmachineblockdeviceattachment.yaml +++ b/crds/virtualmachineblockdeviceattachment.yaml @@ -46,7 +46,6 @@ spec: type: object description: | The block device that will be connected as a hot plug disk to the virtual machine. - required: ["kind", "name"] properties: kind: @@ -63,6 +62,37 @@ spec: status: type: object properties: + conditions: + description: | + The latest available observations of an object's current state. + type: array + items: + type: object + properties: + lastProbeTime: + description: Last time the condition was checked. + format: date-time + type: string + lastTransitionTime: + description: Last time the condition transit from one status to another. + format: date-time + type: string + message: + description: Human readable message indicating details about last transition. + type: string + reason: + description: (brief) reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + enum: ["True", "False", "Unknown"] + type: + description: Type of condition. + type: string + required: + - status + - type virtualMachineName: type: string description: | @@ -72,21 +102,21 @@ spec: description: | Represents the current phase of resource: + * Pending - the resource has been created and is on a waiting queue. * InProgress - the disk is in the process of being attached. * Attached - the disk is attached to virtual machine. - * Failed - there was a problem with attaching the disk. See `.status.failureReason`. + * Failed - there was a problem with attaching the disk. + * Terminating - The process of resource deletion is in progress. enum: + - "Pending" - "InProgress" - "Attached" - "Failed" - failureReason: - type: string - description: | - A brief description of the cause of the error. - failureMessage: - type: string + - "Terminating" + observedGeneration: + type: integer description: | - Detailed description of the error. + The generation last processed by the controller. additionalPrinterColumns: - name: Phase type: string diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index 439bc70b8..7d4e9d43e 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -40,13 +40,13 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/common" appconfig "github.com/deckhouse/virtualization-controller/pkg/config" - "github.com/deckhouse/virtualization-controller/pkg/controller" "github.com/deckhouse/virtualization-controller/pkg/controller/cpu" "github.com/deckhouse/virtualization-controller/pkg/controller/cvi" "github.com/deckhouse/virtualization-controller/pkg/controller/ipam" "github.com/deckhouse/virtualization-controller/pkg/controller/vd" "github.com/deckhouse/virtualization-controller/pkg/controller/vi" "github.com/deckhouse/virtualization-controller/pkg/controller/vm" + "github.com/deckhouse/virtualization-controller/pkg/controller/vmbda" "github.com/deckhouse/virtualization-controller/pkg/controller/vmop" virtv2alpha1 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -188,16 +188,16 @@ func main() { os.Exit(1) } - if _, err := vi.NewController(ctx, mgr, log, importerImage, uploaderImage, dvcrSettings); err != nil { + if _, err = vi.NewController(ctx, mgr, log, importerImage, uploaderImage, dvcrSettings); err != nil { log.Error(err, "") os.Exit(1) } - if _, err := vm.NewController(ctx, mgr, slog.Default(), dvcrSettings); err != nil { + if _, err = vm.NewController(ctx, mgr, slog.Default(), dvcrSettings); err != nil { log.Error(err, "") os.Exit(1) } - if _, err := controller.NewVMBDAController(ctx, mgr, log, controllerNamespace); err != nil { + if _, err = vmbda.NewController(ctx, mgr, log, controllerNamespace); err != nil { log.Error(err, "") os.Exit(1) } diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index dc6e56553..892e18e64 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -18,6 +18,7 @@ package kvbuilder import ( "fmt" + "strings" "k8s.io/apimachinery/pkg/runtime/schema" virtv1 "kubevirt.io/api/core/v1" @@ -49,6 +50,14 @@ func GenerateCVMIDiskName(name string) string { return CVMIDiskPrefix + name } +func GerOriginalDiskName(prefixedName string) (string, bool) { + if strings.HasPrefix(prefixedName, VMDDiskPrefix) { + return strings.TrimPrefix(prefixedName, VMDDiskPrefix), true + } + + return prefixedName, false +} + type HotPlugDeviceSettings struct { VolumeName string PVCName string diff --git a/images/virtualization-artifact/pkg/controller/service/attachment_service.go b/images/virtualization-artifact/pkg/controller/service/attachment_service.go new file mode 100644 index 000000000..083d11448 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/attachment_service.go @@ -0,0 +1,204 @@ +/* +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 service + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/controller/kubevirt" + "github.com/deckhouse/virtualization-controller/pkg/controller/kvapi" + "github.com/deckhouse/virtualization-controller/pkg/controller/kvbuilder" + "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type AttachmentService struct { + client client.Client + controllerNamespace string +} + +func NewAttachmentService(client client.Client, controllerNamespace string) *AttachmentService { + return &AttachmentService{ + client: client, + controllerNamespace: controllerNamespace, + } +} + +var ErrVolumeStatusNotReady = errors.New("hotplug is not ready") + +func (s AttachmentService) IsHotPlugged(vd *virtv2.VirtualDisk, vm *virtv2.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance) (bool, error) { + if vd == nil { + return false, errors.New("cannot check if a nil VirtualDisk is hot plugged") + } + + if vm == nil { + return false, errors.New("cannot check if a disk is hot plugged into a nil VirtualMachine") + } + + if kvvmi == nil { + return false, errors.New("cannot check if a disk is hot plugged into a nil KVVMI") + } + + for _, vs := range kvvmi.Status.VolumeStatus { + if vs.HotplugVolume != nil && vs.Name == kvbuilder.GenerateVMDDiskName(vd.Name) { + if vs.Phase == virtv1.VolumeReady { + return true, nil + } + + return false, fmt.Errorf("%w: %s", ErrVolumeStatusNotReady, vs.Message) + } + } + + return false, nil +} + +func (s AttachmentService) IsHotPlugRequestSent(vd *virtv2.VirtualDisk, kvvm *virtv1.VirtualMachine) (bool, error) { + name := kvbuilder.GenerateVMDDiskName(vd.Name) + + for _, vr := range kvvm.Status.VolumeRequests { + if vr.AddVolumeOptions.Name == name { + return true, nil + } + } + + if kvvm.Spec.Template != nil { + for _, vs := range kvvm.Spec.Template.Spec.Volumes { + if vs.Name == name { + if vs.PersistentVolumeClaim == nil { + return false, fmt.Errorf("kvvm %s/%s spec volume %s does not have a pvc reference", kvvm.Namespace, kvvm.Name, vs.Name) + } + + if !vs.PersistentVolumeClaim.Hotpluggable { + return false, fmt.Errorf("kvvm %s/%s spec volume %s has a pvc reference, but it is not a hot-plugged volume", kvvm.Namespace, kvvm.Name, vs.Name) + } + + return true, nil + } + } + } + + return false, nil +} + +var ( + ErrVirtualDiskIsAlreadyAttached = errors.New("virtual disk is already attached to virtual machine") + ErrVirtualMachineWaitsForRestartApproval = errors.New("virtual machine waits for restart approval") +) + +func (s AttachmentService) HotPlugDisk(ctx context.Context, vd *virtv2.VirtualDisk, vm *virtv2.VirtualMachine) error { + if vd == nil { + return errors.New("cannot hot plug a nil VirtualDisk") + } + + if vm == nil { + return errors.New("cannot hot plug a disk into a nil VirtualMachine") + } + + for _, bdr := range vm.Spec.BlockDeviceRefs { + if bdr.Kind == virtv2.DiskDevice && bdr.Name == vd.Name { + return ErrVirtualDiskIsAlreadyAttached + } + } + + if len(vm.Status.RestartAwaitingChanges) > 0 { + return ErrVirtualMachineWaitsForRestartApproval + } + + name := kvbuilder.GenerateVMDDiskName(vd.Name) + + hotplugRequest := virtv1.AddVolumeOptions{ + Name: name, + Disk: &virtv1.Disk{ + Name: name, + DiskDevice: virtv1.DiskDevice{ + Disk: &virtv1.DiskTarget{ + Bus: "scsi", + }, + }, + Serial: vd.Name, + }, + VolumeSource: &virtv1.HotplugVolumeSource{ + PersistentVolumeClaim: &virtv1.PersistentVolumeClaimVolumeSource{ + PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: vd.Status.Target.PersistentVolumeClaim, + }, + Hotpluggable: true, + }, + }, + } + + kv, err := kubevirt.New(ctx, s.client, s.controllerNamespace) + if err != nil { + return err + } + + err = kvapi.New(s.client, kv).AddVolume(ctx, vm.Namespace, vm.Name, &hotplugRequest) + if err != nil { + return fmt.Errorf("error adding volume, %w", err) + } + + return nil +} + +func (s AttachmentService) UnplugDisk(ctx context.Context, vd *virtv2.VirtualDisk, vm *virtv2.VirtualMachine) error { + if vd == nil || vm == nil { + return nil + } + + unplugRequest := virtv1.RemoveVolumeOptions{ + Name: kvbuilder.GenerateVMDDiskName(vd.Name), + } + + kv, err := kubevirt.New(ctx, s.client, s.controllerNamespace) + if err != nil { + return err + } + + err = kvapi.New(s.client, kv).RemoveVolume(ctx, vm.Namespace, vm.Name, &unplugRequest) + if err != nil { + return fmt.Errorf("error removing volume, %w", err) + } + + return nil +} + +func (s AttachmentService) GetVirtualDisk(ctx context.Context, name, namespace string) (*virtv2.VirtualDisk, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &virtv2.VirtualDisk{}) +} + +func (s AttachmentService) GetPersistentVolumeClaim(ctx context.Context, vd *virtv2.VirtualDisk) (*corev1.PersistentVolumeClaim, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: vd.Namespace, Name: vd.Status.Target.PersistentVolumeClaim}, s.client, &corev1.PersistentVolumeClaim{}) +} + +func (s AttachmentService) GetVirtualMachine(ctx context.Context, name, namespace string) (*virtv2.VirtualMachine, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &virtv2.VirtualMachine{}) +} + +func (s AttachmentService) GetKVVM(ctx context.Context, vm *virtv2.VirtualMachine) (*virtv1.VirtualMachine, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: vm.Namespace, Name: vm.Name}, s.client, &virtv1.VirtualMachine{}) +} + +func (s AttachmentService) GetKVVMI(ctx context.Context, vm *virtv2.VirtualMachine) (*virtv1.VirtualMachineInstance, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: vm.Namespace, Name: vm.Name}, s.client, &virtv1.VirtualMachineInstance{}) +} diff --git a/images/virtualization-artifact/pkg/controller/service/disk_service.go b/images/virtualization-artifact/pkg/controller/service/disk_service.go index f9f1f2e75..a979d900c 100644 --- a/images/virtualization-artifact/pkg/controller/service/disk_service.go +++ b/images/virtualization-artifact/pkg/controller/service/disk_service.go @@ -170,14 +170,24 @@ func (s DiskService) CleanUpSupplements(ctx context.Context, sup *supplements.Ge func (s DiskService) Protect(ctx context.Context, owner client.Object, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim, pv *corev1.PersistentVolume) error { err := s.protection.AddOwnerRef(ctx, owner, pvc) if err != nil { - return err + return fmt.Errorf("failed to add owner ref for pvc: %w", err) } - return s.protection.AddProtection(ctx, dv, pvc, pv) + err = s.protection.AddProtection(ctx, dv, pvc, pv) + if err != nil { + return fmt.Errorf("failed to add protection for disk's supplements: %w", err) + } + + return nil } func (s DiskService) Unprotect(ctx context.Context, dv *cdiv1.DataVolume) error { - return s.protection.RemoveProtection(ctx, dv) + err := s.protection.RemoveProtection(ctx, dv) + if err != nil { + return fmt.Errorf("failed to remove protection for disk's supplements: %w", err) + } + + return nil } func (s DiskService) Resize(ctx context.Context, pvc *corev1.PersistentVolumeClaim, newSize resource.Quantity) error { diff --git a/images/virtualization-artifact/pkg/controller/service/importer_service.go b/images/virtualization-artifact/pkg/controller/service/importer_service.go index b0cc7bdfc..b152a08fe 100644 --- a/images/virtualization-artifact/pkg/controller/service/importer_service.go +++ b/images/virtualization-artifact/pkg/controller/service/importer_service.go @@ -18,6 +18,7 @@ package service import ( "context" + "fmt" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -101,11 +102,21 @@ func (s ImporterService) CleanUpSupplements(ctx context.Context, sup *supplement } func (s ImporterService) Protect(ctx context.Context, pod *corev1.Pod) error { - return s.protection.AddProtection(ctx, pod) + err := s.protection.AddProtection(ctx, pod) + if err != nil { + return fmt.Errorf("failed to add protection for importer's supplements: %w", err) + } + + return nil } func (s ImporterService) Unprotect(ctx context.Context, pod *corev1.Pod) error { - return s.protection.RemoveProtection(ctx, pod) + err := s.protection.RemoveProtection(ctx, pod) + if err != nil { + return fmt.Errorf("failed to remove protection for importer's supplements: %w", err) + } + + return nil } func (s ImporterService) GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { diff --git a/images/virtualization-artifact/pkg/controller/service/resource.go b/images/virtualization-artifact/pkg/controller/service/resource.go index fc91f9f4e..e4675c91c 100644 --- a/images/virtualization-artifact/pkg/controller/service/resource.go +++ b/images/virtualization-artifact/pkg/controller/service/resource.go @@ -20,13 +20,13 @@ import ( "context" "encoding/json" "fmt" - "log/slog" "reflect" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/strings/slices" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" ) @@ -109,7 +109,6 @@ func (r *Resource[T, ST]) Update(ctx context.Context) error { finalizers := r.changedObj.GetFinalizers() if !reflect.DeepEqual(r.getObjStatus(r.currentObj), r.getObjStatus(r.changedObj)) { - slog.Info("Update Status") if err := r.client.Status().Update(ctx, r.changedObj); err != nil { return fmt.Errorf("error updating status subresource: %w", err) } @@ -117,8 +116,6 @@ func (r *Resource[T, ST]) Update(ctx context.Context) error { } if !slices.Equal(r.currentObj.GetFinalizers(), r.changedObj.GetFinalizers()) { - slog.Info("Patch Finalizers") - patch, err := GetPatchFinalizers(r.changedObj.GetFinalizers()) if err != nil { return err @@ -157,3 +154,23 @@ func GetPatchFinalizers(finalizers []string) (client.Patch, error) { return client.RawPatch(types.MergePatchType, data), nil } + +func MergeResults(results ...reconcile.Result) reconcile.Result { + var result reconcile.Result + for _, r := range results { + if r.IsZero() { + continue + } + if r.Requeue && r.RequeueAfter == 0 { + return r + } + if result.IsZero() && r.RequeueAfter > 0 { + result = r + continue + } + if r.RequeueAfter > 0 && r.RequeueAfter < result.RequeueAfter { + result.RequeueAfter = r.RequeueAfter + } + } + return result +} diff --git a/images/virtualization-artifact/pkg/controller/service/uploader_service.go b/images/virtualization-artifact/pkg/controller/service/uploader_service.go index fc9117e09..94fa775ba 100644 --- a/images/virtualization-artifact/pkg/controller/service/uploader_service.go +++ b/images/virtualization-artifact/pkg/controller/service/uploader_service.go @@ -18,6 +18,7 @@ package service import ( "context" + "fmt" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" @@ -141,11 +142,21 @@ func (s UploaderService) CleanUpSupplements(ctx context.Context, sup *supplement } func (s UploaderService) Protect(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { - return s.protection.AddProtection(ctx, pod, svc, ing) + err := s.protection.AddProtection(ctx, pod, svc, ing) + if err != nil { + return fmt.Errorf("failed to add protection for uploader's supplements: %w", err) + } + + return nil } func (s UploaderService) Unprotect(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { - return s.protection.RemoveProtection(ctx, pod, svc, ing) + err := s.protection.RemoveProtection(ctx, pod, svc, ing) + if err != nil { + return fmt.Errorf("failed to remove protection for uploader's supplements: %w", err) + } + + return nil } func (s UploaderService) GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/block_device.go b/images/virtualization-artifact/pkg/controller/vm/internal/block_device.go index 5eed02fc6..a03c81edb 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/block_device.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/block_device.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "log/slog" - "slices" "strings" "time" @@ -109,28 +108,55 @@ func (h *BlockDeviceHandler) Handle(ctx context.Context, s state.VirtualMachineS } if kvvmi != nil { - specDeviceMap := make(map[virtv2.BlockDeviceSpecRef]struct{}, len(changed.Spec.BlockDeviceRefs)) + // Fill BlockDeviceRefs every time without knowledge of previously kept BlockDeviceRefs. + changed.Status.BlockDeviceRefs = nil + + // Set BlockDeviceRef in the status if the disk exists in KVVMI. for _, ref := range changed.Spec.BlockDeviceRefs { - specDeviceMap[ref] = struct{}{} - idx, bds := h.findAttachedBlockDevice(changed, ref) - newBds := h.createAttachedBlockDevice(ref, bdState, kvvmi) - if newBds == nil { + bd := h.createAttachedBlockDevice(ref, bdState, kvvmi) + if bd == nil { continue } - if bds != nil { - changed.Status.BlockDeviceRefs[idx] = *newBds + + changed.Status.BlockDeviceRefs = append( + changed.Status.BlockDeviceRefs, + *bd, + ) + } + + // Set BlockDeviceRef `Hotpluggable: true` in the status if KVVMI has a hotplugged disk. + for _, vs := range kvvmi.Status.VolumeStatus { + if vs.HotplugVolume == nil { continue } + + vdName, ok := kvbuilder.GerOriginalDiskName(vs.Name) + if !ok { + h.logger.Warn("volume %s was hot plugged to VirtualMachineInstance %s, but it is not a VirtualDisk.", vdName, kvvmi.Name) + h.recorder.Eventf(changed, corev1.EventTypeNormal, virtv2.ReasonUnknownHotPluggedVolume, "Volume %s was hot plugged to VirtualMachineInstance %s, but it is not a VirtualDisk.", vdName, kvvmi.Name) + continue + } + + var vd *virtv2.VirtualDisk + vd, err = s.VirtualDisk(ctx, vdName) + if err != nil { + return reconcile.Result{}, err + } + + if vd == nil { + h.logger.Warn("VirtualDisk %s not found but pvc hot plugged into VirtualMachineInstance %s", vdName, kvvmi.Name) + h.recorder.Eventf(changed, corev1.EventTypeNormal, virtv2.ReasonUnknownHotPluggedVolume, "VirtualDisk %s not found but pvc hot plugged into VirtualMachineInstance %s", vdName, kvvmi.Name) + continue + } + changed.Status.BlockDeviceRefs = append( changed.Status.BlockDeviceRefs, - *newBds) + h.getHotPluggedDiskStatusRef(vs, vd), + ) } - - changed.Status.BlockDeviceRefs = slices.DeleteFunc(changed.Status.BlockDeviceRefs, func(ref virtv2.BlockDeviceStatusRef) bool { - _, ok := specDeviceMap[virtv2.BlockDeviceSpecRef{Kind: ref.Kind, Name: ref.Name}] - return !ok && h.findVolumeStatus(GenerateDiskName(ref.Kind, ref.Name), kvvmi) == nil - }) } + + // Update the BlockDevicesReady condition. countBD := len(current.Spec.BlockDeviceRefs) if ready, count := h.countReadyBlockDevices(current, bdState); !ready { // Wait until block devices are ready. @@ -267,6 +293,16 @@ func (h *BlockDeviceHandler) updateFinalizers(ctx context.Context, vm *virtv2.Vi return nil } +func (h *BlockDeviceHandler) getHotPluggedDiskStatusRef(vs virtv1.VolumeStatus, vd *virtv2.VirtualDisk) virtv2.BlockDeviceStatusRef { + return virtv2.BlockDeviceStatusRef{ + Kind: virtv2.DiskDevice, + Name: vd.Name, + Target: vs.Target, + Size: vd.Status.Capacity, + Hotpluggable: true, + } +} + func (h *BlockDeviceHandler) createAttachedBlockDevice(spec virtv2.BlockDeviceSpecRef, state BlockDevicesState, kvvmi *virtv1.VirtualMachineInstance) *virtv2.BlockDeviceStatusRef { if kvvmi == nil { return nil @@ -329,17 +365,6 @@ func (h *BlockDeviceHandler) createAttachedBlockDevice(spec virtv2.BlockDeviceSp return nil } -func (h *BlockDeviceHandler) findAttachedBlockDevice(vm *virtv2.VirtualMachine, spec virtv2.BlockDeviceSpecRef) (int, *virtv2.BlockDeviceStatusRef) { - for i := range vm.Status.BlockDeviceRefs { - bda := &vm.Status.BlockDeviceRefs[i] - if bda.Kind == spec.Kind && bda.Name == spec.Name { - return i, bda - } - } - - return -1, nil -} - func (h *BlockDeviceHandler) findVolumeStatus(volumeName string, kvvmi *virtv1.VirtualMachineInstance) *virtv1.VolumeStatus { if kvvmi != nil { for i := range kvvmi.Status.VolumeStatus { @@ -369,11 +394,11 @@ type BlockDevicesState struct { } func (s *BlockDevicesState) Reload(ctx context.Context) error { - viByName, err := s.s.VirtualImageByName(ctx) + viByName, err := s.s.VirtualImagesByName(ctx) if err != nil { return err } - ciByName, err := s.s.ClusterVirtualImageByName(ctx) + ciByName, err := s.s.ClusterVirtualImagesByName(ctx) if err != nil { return err } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/state/state.go b/images/virtualization-artifact/pkg/controller/vm/internal/state/state.go index 781a2698c..390bf394e 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/state/state.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/state/state.go @@ -40,9 +40,10 @@ type VirtualMachineState interface { KVVMI(ctx context.Context) (*virtv1.VirtualMachineInstance, error) Pods(ctx context.Context) (*corev1.PodList, error) Pod(ctx context.Context) (*corev1.Pod, error) + VirtualDisk(ctx context.Context, name string) (*virtv2.VirtualDisk, error) VirtualDisksByName(ctx context.Context) (map[string]*virtv2.VirtualDisk, error) - VirtualImageByName(ctx context.Context) (map[string]*virtv2.VirtualImage, error) - ClusterVirtualImageByName(ctx context.Context) (map[string]*virtv2.ClusterVirtualImage, error) + VirtualImagesByName(ctx context.Context) (map[string]*virtv2.VirtualImage, error) + ClusterVirtualImagesByName(ctx context.Context) (map[string]*virtv2.ClusterVirtualImage, error) IPAddressClaim(ctx context.Context) (*virtv2.VirtualMachineIPAddressClaim, error) CPUModel(ctx context.Context) (*virtv2.VirtualMachineCPUModel, error) } @@ -153,6 +154,13 @@ func (s *state) Pod(ctx context.Context) (*corev1.Pod, error) { return pod, nil } +func (s *state) VirtualDisk(ctx context.Context, name string) (*virtv2.VirtualDisk, error) { + return helper.FetchObject(ctx, types.NamespacedName{ + Name: name, + Namespace: s.vm.Current().GetNamespace(), + }, s.client, &virtv2.VirtualDisk{}) +} + func (s *state) VirtualDisksByName(ctx context.Context) (map[string]*virtv2.VirtualDisk, error) { if s.vm == nil { return nil, nil @@ -185,7 +193,7 @@ func (s *state) VirtualDisksByName(ctx context.Context) (map[string]*virtv2.Virt return vdByName, nil } -func (s *state) VirtualImageByName(ctx context.Context) (map[string]*virtv2.VirtualImage, error) { +func (s *state) VirtualImagesByName(ctx context.Context) (map[string]*virtv2.VirtualImage, error) { if s.vm == nil { return nil, nil } @@ -217,7 +225,7 @@ func (s *state) VirtualImageByName(ctx context.Context) (map[string]*virtv2.Virt return viByName, nil } -func (s *state) ClusterVirtualImageByName(ctx context.Context) (map[string]*virtv2.ClusterVirtualImage, error) { +func (s *state) ClusterVirtualImagesByName(ctx context.Context) (map[string]*virtv2.ClusterVirtualImage, error) { if s.vm == nil { return nil, nil } diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go index 20edb8480..a96a7b863 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go @@ -314,7 +314,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco r.logger.Error("The handler failed with an error", slog.String("name", h.Name()), log.SlogErr(err)) handlerErr = errors.Join(handlerErr, err) } - result = MergeResults(result, res) + result = service.MergeResults(result, res) } if handlerErr != nil { err = r.updateVM(ctx, vm) @@ -343,23 +343,3 @@ func (r *Reconciler) factory() *virtv2.VirtualMachine { func (r *Reconciler) statusGetter(obj *virtv2.VirtualMachine) virtv2.VirtualMachineStatus { return obj.Status } - -func MergeResults(results ...reconcile.Result) reconcile.Result { - var result reconcile.Result - for _, r := range results { - if r.IsZero() { - continue - } - if r.Requeue { - return r - } - if result.IsZero() && r.RequeueAfter > 0 { - result = r - continue - } - if r.RequeueAfter > 0 && r.RequeueAfter < result.RequeueAfter { - result.RequeueAfter = r.RequeueAfter - } - } - return result -} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/block_device_ready.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/block_device_ready.go new file mode 100644 index 000000000..941906ac6 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/block_device_ready.go @@ -0,0 +1,130 @@ +/* +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 internal + +import ( + "context" + "fmt" + + 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/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmbdacondition" +) + +type BlockDeviceReadyHandler struct { + attachment *service.AttachmentService +} + +func NewBlockDeviceReadyHandler(attachment *service.AttachmentService) *BlockDeviceReadyHandler { + return &BlockDeviceReadyHandler{ + attachment: attachment, + } +} + +func (h BlockDeviceReadyHandler) Handle(ctx context.Context, vmbda *virtv2.VirtualMachineBlockDeviceAttachment) (reconcile.Result, error) { + condition, ok := service.GetCondition(vmbdacondition.BlockDeviceReadyType, vmbda.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vmbdacondition.BlockDeviceReadyType, + Status: metav1.ConditionUnknown, + } + } + + defer func() { service.SetCondition(condition, &vmbda.Status.Conditions) }() + + if vmbda.DeletionTimestamp != nil { + condition.Status = metav1.ConditionUnknown + condition.Reason = "" + condition.Message = "" + return reconcile.Result{}, nil + } + + switch vmbda.Spec.BlockDeviceRef.Kind { + case virtv2.VMBDAObjectRefKindVirtualDisk: + vdKey := types.NamespacedName{ + Name: vmbda.Spec.BlockDeviceRef.Name, + Namespace: vmbda.Namespace, + } + + vd, err := h.attachment.GetVirtualDisk(ctx, vdKey.Name, vdKey.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + if vd == nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.BlockDeviceNotReady + condition.Message = fmt.Sprintf("VirtualDisk %s not found.", vdKey.String()) + return reconcile.Result{}, nil + } + + if vd.Generation != vd.Status.ObservedGeneration { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.BlockDeviceNotReady + condition.Message = fmt.Sprintf("Waiting for the VirtualDisk %s to be observed in its latest state generation.", vdKey.String()) + return reconcile.Result{}, nil + } + + var diskReadyCondition metav1.Condition + diskReadyCondition, ok = service.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + if !ok || diskReadyCondition.Status != metav1.ConditionTrue { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.BlockDeviceNotReady + condition.Message = fmt.Sprintf("VirtualDisk %s is not Ready: waiting for the VirtualDisk to be Ready.", vdKey.String()) + return reconcile.Result{}, nil + } + + if vd.Status.Target.PersistentVolumeClaim == "" { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.BlockDeviceNotReady + condition.Message = "Waiting until VirtualDisk has associated PersistentVolumeClaim name." + return reconcile.Result{}, nil + } + + pvc, err := h.attachment.GetPersistentVolumeClaim(ctx, vd) + if err != nil { + return reconcile.Result{}, err + } + + if pvc == nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.BlockDeviceNotReady + condition.Message = fmt.Sprintf("Underlying PersistentVolumeClaim %s not found.", vd.Status.Target) + return reconcile.Result{}, nil + } + + if pvc.Status.Phase != corev1.ClaimBound { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.BlockDeviceNotReady + condition.Message = fmt.Sprintf("Underlying PersistentVolumeClaim %s not bound.", vd.Status.Target) + return reconcile.Result{}, nil + } + + condition.Status = metav1.ConditionTrue + condition.Reason = vmbdacondition.BlockDeviceReady + condition.Message = "" + return reconcile.Result{}, nil + default: + return reconcile.Result{}, fmt.Errorf("unknown block device kind %s", vmbda.Spec.BlockDeviceRef.Kind) + } +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/deletion.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/deletion.go new file mode 100644 index 000000000..5830d4cfd --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/deletion.go @@ -0,0 +1,42 @@ +/* +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 internal + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type DeletionHandler struct{} + +func NewDeletionHandler() *DeletionHandler { + return &DeletionHandler{} +} + +func (h DeletionHandler) Handle(_ context.Context, vd *virtv2.VirtualMachineBlockDeviceAttachment) (reconcile.Result, error) { + if vd.DeletionTimestamp != nil { + controllerutil.RemoveFinalizer(vd, virtv2.FinalizerVMBDACleanup) + return reconcile.Result{}, nil + } + + controllerutil.AddFinalizer(vd, virtv2.FinalizerVMBDACleanup) + return reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go new file mode 100644 index 000000000..8851af3df --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/life_cycle.go @@ -0,0 +1,209 @@ +/* +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 internal + +import ( + "context" + "errors" + "fmt" + "log/slog" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmbdacondition" +) + +type LifeCycleHandler struct { + attacher *service.AttachmentService + logger *slog.Logger +} + +func NewLifeCycleHandler(logger *slog.Logger, attacher *service.AttachmentService) *LifeCycleHandler { + return &LifeCycleHandler{ + logger: logger, + attacher: attacher, + } +} + +func (h LifeCycleHandler) Handle(ctx context.Context, vmbda *virtv2.VirtualMachineBlockDeviceAttachment) (reconcile.Result, error) { + logger := h.logger.With("name", vmbda.Name, "ns", vmbda.Namespace) + logger.Info("Sync") + // TODO protect vd. + + condition, ok := service.GetCondition(vmbdacondition.AttachedType, vmbda.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vmbdacondition.AttachedType, + Status: metav1.ConditionUnknown, + } + } + + defer func() { service.SetCondition(condition, &vmbda.Status.Conditions) }() + + vd, err := h.attacher.GetVirtualDisk(ctx, vmbda.Spec.BlockDeviceRef.Name, vmbda.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + vm, err := h.attacher.GetVirtualMachine(ctx, vmbda.Spec.VirtualMachineName, vmbda.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + if vmbda.DeletionTimestamp != nil { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhaseTerminating + condition.Status = metav1.ConditionUnknown + condition.Reason = "" + condition.Message = "" + + err = h.attacher.UnplugDisk(ctx, vd, vm) + if err != nil { + return reconcile.Result{}, err + } + + return reconcile.Result{}, nil + } + + if vmbda.Status.Phase == "" { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + } + + blockDeviceReady, ok := service.GetCondition(vmbdacondition.BlockDeviceReadyType, vmbda.Status.Conditions) + if !ok || blockDeviceReady.Status != metav1.ConditionTrue { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.NotAttached + condition.Message = "Waiting for block device to be ready." + return reconcile.Result{}, nil + } + + virtualMachineReady, ok := service.GetCondition(vmbdacondition.VirtualMachineReadyType, vmbda.Status.Conditions) + if !ok || virtualMachineReady.Status != metav1.ConditionTrue { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.NotAttached + condition.Message = "Waiting for virtual machine to be ready." + return reconcile.Result{}, nil + } + + if vd == nil { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.NotAttached + condition.Message = fmt.Sprintf("VirtualDisk %s not found.", vmbda.Spec.BlockDeviceRef.Name) + return reconcile.Result{}, nil + } + + if vm == nil { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.NotAttached + condition.Message = fmt.Sprintf("VirtualMachine %s not found.", vmbda.Spec.VirtualMachineName) + return reconcile.Result{}, nil + } + + kvvm, err := h.attacher.GetKVVM(ctx, vm) + if err != nil { + return reconcile.Result{}, err + } + + if kvvm == nil { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.NotAttached + condition.Message = fmt.Sprintf("InternalVirtualizationVirtualMachine %s not found.", vm.Name) + return reconcile.Result{}, nil + } + + kvvmi, err := h.attacher.GetKVVMI(ctx, vm) + if err != nil { + return reconcile.Result{}, err + } + + if kvvmi == nil { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.NotAttached + condition.Message = fmt.Sprintf("InternalVirtualizationVirtualMachineInstance %s not found.", vm.Name) + return reconcile.Result{}, nil + } + + logger = logger.With("vmName", vm.Name, "vdName", vd.Name) + logger.Info("Check if hot plug is completed and disk is attached") + + isHotPlugged, err := h.attacher.IsHotPlugged(vd, vm, kvvmi) + if err != nil { + if errors.Is(err, service.ErrVolumeStatusNotReady) { + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhaseInProgress + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.AttachmentRequestSent + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return reconcile.Result{}, nil + } + + return reconcile.Result{}, err + } + + if isHotPlugged { + logger.Info("Hot plug is completed and disk is attached") + + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhaseAttached + condition.Status = metav1.ConditionTrue + condition.Reason = vmbdacondition.Attached + condition.Message = "" + return reconcile.Result{}, nil + } + + isHotPlugRequestSent, err := h.attacher.IsHotPlugRequestSent(vd, kvvm) + if err != nil { + return reconcile.Result{}, err + } + + if isHotPlugRequestSent { + logger.Info("Attachment request sent: attachment is in progress.") + + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhaseInProgress + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.AttachmentRequestSent + condition.Message = "Attachment request sent: attachment is in progress." + return reconcile.Result{}, nil + } + + logger.Info("Send attachment request") + + err = h.attacher.HotPlugDisk(ctx, vd, vm) + switch { + case err == nil: + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhaseInProgress + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.AttachmentRequestSent + condition.Message = "Attachment request has sent: attachment is in progress." + return reconcile.Result{}, nil + case errors.Is(err, service.ErrVirtualDiskIsAlreadyAttached), + errors.Is(err, service.ErrVirtualMachineWaitsForRestartApproval): + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.NotAttached + condition.Message = service.CapitalizeFirstLetter(err.Error()) + return reconcile.Result{}, nil + default: + return reconcile.Result{}, err + } +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready.go new file mode 100644 index 000000000..b40645e4c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/virtual_machine_ready.go @@ -0,0 +1,121 @@ +/* +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 internal + +import ( + "context" + "fmt" + + 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/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmbdacondition" +) + +type VirtualMachineReadyHandler struct { + attachment *service.AttachmentService +} + +func NewVirtualMachineReadyHandler(attachment *service.AttachmentService) *VirtualMachineReadyHandler { + return &VirtualMachineReadyHandler{ + attachment: attachment, + } +} + +func (h VirtualMachineReadyHandler) Handle(ctx context.Context, vmbda *virtv2.VirtualMachineBlockDeviceAttachment) (reconcile.Result, error) { + condition, ok := service.GetCondition(vmbdacondition.VirtualMachineReadyType, vmbda.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vmbdacondition.VirtualMachineReadyType, + Status: metav1.ConditionUnknown, + } + } + + defer func() { service.SetCondition(condition, &vmbda.Status.Conditions) }() + + if vmbda.DeletionTimestamp != nil { + condition.Status = metav1.ConditionUnknown + condition.Reason = "" + condition.Message = "" + return reconcile.Result{}, nil + } + + vmKey := types.NamespacedName{ + Name: vmbda.Spec.VirtualMachineName, + Namespace: vmbda.Namespace, + } + + vm, err := h.attachment.GetVirtualMachine(ctx, vmbda.Spec.VirtualMachineName, vmbda.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + if vm == nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.VirtualMachineNotReady + condition.Message = fmt.Sprintf("VirtualMachine %s not found.", vmKey.String()) + return reconcile.Result{}, nil + } + + switch vm.Status.Phase { + case virtv2.MachineRunning: + // OK. + case virtv2.MachineStopping, virtv2.MachineStopped, virtv2.MachineStarting: + vmbda.Status.Phase = virtv2.BlockDeviceAttachmentPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.NotAttached + condition.Message = fmt.Sprintf("VirtualMachine %s is %s: waiting for the VirtualMachine to be Running.", vm.Name, vm.Status.Phase) + return reconcile.Result{}, nil + default: + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.VirtualMachineNotReady + condition.Message = fmt.Sprintf("Waiting for the VirtualMachine %s to be Running.", vmKey.String()) + return reconcile.Result{}, nil + } + + kvvm, err := h.attachment.GetKVVM(ctx, vm) + if err != nil { + return reconcile.Result{}, err + } + + if kvvm == nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.VirtualMachineNotReady + condition.Message = fmt.Sprintf("VirtualMachine %s Running, but underlying InternalVirtualizationVirtualMachine not found.", vmKey.String()) + return reconcile.Result{}, nil + } + + kvvmi, err := h.attachment.GetKVVMI(ctx, vm) + if err != nil { + return reconcile.Result{}, err + } + + if kvvmi == nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vmbdacondition.VirtualMachineNotReady + condition.Message = fmt.Sprintf("VirtualMachine %s Running, but underlying InternalVirtualizationVirtualMachineInstance not found.", vmKey.String()) + return reconcile.Result{}, nil + } + + condition.Status = metav1.ConditionTrue + condition.Reason = vmbdacondition.VirtualMachineReady + condition.Message = "" + return reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/kvvmi_watcher.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/kvvmi_watcher.go new file mode 100644 index 000000000..d63c6ea2e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/kvvmi_watcher.go @@ -0,0 +1,157 @@ +/* +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 watcher + +import ( + "context" + "fmt" + "log/slog" + "reflect" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + virtv1 "kubevirt.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/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/virtualization-controller/pkg/controller/kvbuilder" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type KVVMIWatcher struct { + client client.Client + logger *slog.Logger +} + +var _ handler.EventHandler = &KVVMIEventHandler{} + +func NewKVVMIWatcher(logger *slog.Logger, client client.Client) *KVVMIWatcher { + return &KVVMIWatcher{ + client: client, + logger: logger.With("watcher", "kvvmi"), + } +} + +func (w KVVMIWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv1.VirtualMachineInstance{}), + NewKVVMIEventHandler(w.client, w.logger), + ) +} + +type KVVMIEventHandler struct { + client client.Client + logger *slog.Logger +} + +func NewKVVMIEventHandler(client client.Client, logger *slog.Logger) *KVVMIEventHandler { + return &KVVMIEventHandler{ + client: client, + logger: logger, + } +} + +func (eh KVVMIEventHandler) Create(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { + eh.enqueueRequests(ctx, e.Object.GetNamespace(), eh.getHotPluggedVolumeStatuses(e.Object), q) +} + +func (eh KVVMIEventHandler) Delete(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) { + eh.enqueueRequests(ctx, e.Object.GetNamespace(), eh.getHotPluggedVolumeStatuses(e.Object), q) +} + +func (eh KVVMIEventHandler) Update(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { + oldVolumeStatuses := eh.getHotPluggedVolumeStatuses(e.ObjectOld) + newVolumeStatuses := eh.getHotPluggedVolumeStatuses(e.ObjectNew) + + eh.enqueueRequests(ctx, e.ObjectNew.GetNamespace(), getVolumeStatusesToReconcile(oldVolumeStatuses, newVolumeStatuses), q) +} + +func (eh KVVMIEventHandler) Generic(_ context.Context, _ event.GenericEvent, _ workqueue.RateLimitingInterface) { + // Not implemented. +} + +func (eh KVVMIEventHandler) enqueueRequests(ctx context.Context, ns string, vsToReconcile map[string]virtv1.VolumeStatus, q workqueue.RateLimitingInterface) { + if len(vsToReconcile) == 0 { + return + } + + var vmbdas virtv2.VirtualMachineBlockDeviceAttachmentList + err := eh.client.List(ctx, &vmbdas, &client.ListOptions{ + Namespace: ns, + }) + if err != nil { + eh.logger.Error(fmt.Sprintf("failed to list vmbdas: %s", err)) + return + } + + for _, vmbda := range vmbdas.Items { + _, ok := vsToReconcile[vmbda.Spec.BlockDeviceRef.Name] + if ok { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Namespace: vmbda.Namespace, + Name: vmbda.Name, + }}) + } + } +} + +func (eh KVVMIEventHandler) getHotPluggedVolumeStatuses(obj client.Object) map[string]virtv1.VolumeStatus { + kvvmi, ok := obj.(*virtv1.VirtualMachineInstance) + if !ok || kvvmi == nil { + return nil + } + + volumeStatuses := make(map[string]virtv1.VolumeStatus) + + for _, vs := range kvvmi.Status.VolumeStatus { + if vs.HotplugVolume != nil { + var name string + name, ok = kvbuilder.GerOriginalDiskName(vs.Name) + if !ok { + eh.logger.Warn("VolumeStatus is not a Disk", "vsName", vs.Name, "name", kvvmi.Name, "ns", kvvmi.Namespace) + continue + } + + volumeStatuses[name] = vs + } + } + + return volumeStatuses +} + +func getVolumeStatusesToReconcile(oldVolumeStatuses, newVolumeStatuses map[string]virtv1.VolumeStatus) map[string]virtv1.VolumeStatus { + result := make(map[string]virtv1.VolumeStatus) + + for vsName, newVS := range newVolumeStatuses { + if oldVS, ok := oldVolumeStatuses[vsName]; !ok || !reflect.DeepEqual(oldVS, newVS) { + result[vsName] = newVS + } + } + + for vsName, oldVS := range oldVolumeStatuses { + if _, ok := newVolumeStatuses[vsName]; !ok { + result[vsName] = oldVS + } + } + + return result +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vd_watcher.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vd_watcher.go new file mode 100644 index 000000000..e4cbed76b --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vd_watcher.go @@ -0,0 +1,106 @@ +/* +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 watcher + +import ( + "context" + "fmt" + "log/slog" + + "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/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type VirtualDiskWatcher struct { + client client.Client + logger *slog.Logger +} + +func NewVirtualDiskWatcher(logger *slog.Logger, client client.Client) *VirtualDiskWatcher { + return &VirtualDiskWatcher{ + client: client, + logger: logger.With("watcher", "vd"), + } +} + +func (w VirtualDiskWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualDisk{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return false }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualDiskWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var vmbdas virtv2.VirtualMachineBlockDeviceAttachmentList + err := w.client.List(ctx, &vmbdas, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + w.logger.Error(fmt.Sprintf("failed to list vmbdas: %s", err)) + return + } + + for _, vmbda := range vmbdas.Items { + if vmbda.Spec.BlockDeviceRef.Kind != virtv2.VMBDAObjectRefKindVirtualDisk && vmbda.Spec.BlockDeviceRef.Name != obj.GetName() { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vmbda.Name, + Namespace: vmbda.Namespace, + }, + }) + } + + return +} + +func (w VirtualDiskWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVD, ok := e.ObjectOld.(*virtv2.VirtualDisk) + if !ok { + w.logger.Error(fmt.Sprintf("expected an old VirtualDisk but got a %T", e.ObjectOld)) + return false + } + + newVD, ok := e.ObjectNew.(*virtv2.VirtualDisk) + if !ok { + w.logger.Error(fmt.Sprintf("expected a new VirtualDisk but got a %T", e.ObjectNew)) + return false + } + + oldReadyCondition, _ := service.GetCondition(vdcondition.ReadyType, oldVD.Status.Conditions) + newReadyCondition, _ := service.GetCondition(vdcondition.ReadyType, newVD.Status.Conditions) + + return oldReadyCondition.Status != newReadyCondition.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go new file mode 100644 index 000000000..c67876d46 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vm_watcher.go @@ -0,0 +1,106 @@ +/* +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 watcher + +import ( + "context" + "fmt" + "log/slog" + + "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/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +type VirtualMachineWatcher struct { + client client.Client + logger *slog.Logger +} + +func NewVirtualMachineWatcher(logger *slog.Logger, client client.Client) *VirtualMachineWatcher { + return &VirtualMachineWatcher{ + client: client, + logger: logger.With("watcher", "vm"), + } +} + +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 false }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualMachineWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var vmbdas virtv2.VirtualMachineBlockDeviceAttachmentList + err := w.client.List(ctx, &vmbdas, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + w.logger.Error(fmt.Sprintf("failed to list vmbdas: %s", err)) + return + } + + for _, vmbda := range vmbdas.Items { + if vmbda.Spec.VirtualMachineName != obj.GetName() { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vmbda.Name, + Namespace: vmbda.Namespace, + }, + }) + } + + return +} + +func (w VirtualMachineWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVM, ok := e.ObjectOld.(*virtv2.VirtualMachine) + if !ok { + w.logger.Error(fmt.Sprintf("expected an old VirtualMachine but got a %T", e.ObjectOld)) + return false + } + + newVM, ok := e.ObjectNew.(*virtv2.VirtualMachine) + if !ok { + w.logger.Error(fmt.Sprintf("expected a new VirtualMachine but got a %T", e.ObjectNew)) + return false + } + + oldRunningCondition, _ := service.GetCondition(vmcondition.TypeRunning.String(), oldVM.Status.Conditions) + newRunningCondition, _ := service.GetCondition(vmcondition.TypeRunning.String(), newVM.Status.Conditions) + + return newRunningCondition.Status != oldRunningCondition.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vmbda_watcher.go b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vmbda_watcher.go new file mode 100644 index 000000000..21a7732eb --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/internal/watcher/vmbda_watcher.go @@ -0,0 +1,57 @@ +/* +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 watcher + +import ( + "log/slog" + + "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" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineBlockDeviceAttachmentWatcher struct { + client client.Client + logger *slog.Logger +} + +func NewVirtualMachineBlockDeviceAttachmentWatcher(logger *slog.Logger, client client.Client) *VirtualMachineBlockDeviceAttachmentWatcher { + return &VirtualMachineBlockDeviceAttachmentWatcher{ + client: client, + logger: logger.With("watcher", "vmbda"), + } +} + +func (w VirtualMachineBlockDeviceAttachmentWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualMachineBlockDeviceAttachment{}), + &handler.EnqueueRequestForObject{}, + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return true }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: func(e event.UpdateEvent) bool { + return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() + }, + }, + ) +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/vmbda_controller.go b/images/virtualization-artifact/pkg/controller/vmbda/vmbda_controller.go new file mode 100644 index 000000000..f2aa8981a --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/vmbda_controller.go @@ -0,0 +1,92 @@ +/* +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 vmbda + +import ( + "context" + "log/slog" + + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/metrics" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vmbda/internal" + vmbdametrics "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/vmbda" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +const ControllerName = "vmbda-controller" + +func NewController( + ctx context.Context, + mgr manager.Manager, + log logr.Logger, + ns string, +) (controller.Controller, error) { + logger := slog.Default().With("controller", ControllerName) + + attacher := service.NewAttachmentService(mgr.GetClient(), ns) + + reconciler := NewReconciler( + mgr.GetClient(), + logger, + internal.NewBlockDeviceReadyHandler(attacher), + internal.NewVirtualMachineReadyHandler(attacher), + internal.NewLifeCycleHandler(logger, attacher), + internal.NewDeletionHandler(), + ) + + vmbdaController, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: reconciler}) + if err != nil { + return nil, err + } + + err = reconciler.SetupController(ctx, mgr, vmbdaController) + if err != nil { + return nil, err + } + + if err = builder.WebhookManagedBy(mgr). + For(&virtv2.VirtualMachineBlockDeviceAttachment{}). + WithValidator(NewValidator()). + Complete(); err != nil { + return nil, err + } + + vmbdametrics.SetupCollector(&lister{cache: mgr.GetCache()}, metrics.Registry) + + log.Info("Initialized VirtualMachineBlockDeviceAttachment controller") + + return vmbdaController, nil +} + +type lister struct { + cache cache.Cache +} + +func (l lister) List() ([]virtv2.VirtualMachineBlockDeviceAttachment, error) { + vmbdas := virtv2.VirtualMachineBlockDeviceAttachmentList{} + err := l.cache.List(context.Background(), &vmbdas) + if err != nil { + return nil, err + } + return vmbdas.Items, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/vmbda_reconciler.go b/images/virtualization-artifact/pkg/controller/vmbda/vmbda_reconciler.go new file mode 100644 index 000000000..3a045abf5 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/vmbda_reconciler.go @@ -0,0 +1,121 @@ +/* +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 vmbda + +import ( + "context" + "errors" + "fmt" + "log/slog" + "reflect" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vmbda/internal/watcher" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Handler interface { + Handle(ctx context.Context, vmbda *virtv2.VirtualMachineBlockDeviceAttachment) (reconcile.Result, error) +} + +type Watcher interface { + Watch(mgr manager.Manager, ctr controller.Controller) error +} + +type Reconciler struct { + handlers []Handler + client client.Client + logger *slog.Logger +} + +func NewReconciler(client client.Client, logger *slog.Logger, handlers ...Handler) *Reconciler { + return &Reconciler{ + client: client, + logger: logger, + handlers: handlers, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + vmbda := service.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) + + err := vmbda.Fetch(ctx) + if err != nil { + return reconcile.Result{}, err + } + + if vmbda.IsEmpty() { + return reconcile.Result{}, nil + } + + var result reconcile.Result + var handlerErrs []error + + for _, h := range r.handlers { + var res reconcile.Result + res, err = h.Handle(ctx, vmbda.Changed()) + if err != nil { + r.logger.Error("Failed to handle vmbda", "err", err) + handlerErrs = append(handlerErrs, err) + } + + result = service.MergeResults(result, res) + } + + vmbda.Changed().Status.ObservedGeneration = vmbda.Changed().Generation + + err = vmbda.Update(ctx) + if err != nil { + return reconcile.Result{}, err + } + + err = errors.Join(handlerErrs...) + if err != nil { + return reconcile.Result{}, err + } + + return result, nil +} + +func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { + for _, w := range []Watcher{ + watcher.NewVirtualMachineBlockDeviceAttachmentWatcher(r.logger, mgr.GetClient()), + watcher.NewVirtualMachineWatcher(r.logger, mgr.GetClient()), + watcher.NewVirtualDiskWatcher(r.logger, mgr.GetClient()), + watcher.NewKVVMIWatcher(r.logger, mgr.GetClient()), + } { + err := w.Watch(mgr, ctr) + if err != nil { + return fmt.Errorf("faield to run watcher %s: %w", reflect.TypeOf(w).Elem().Name(), err) + } + } + + return nil +} + +func (r *Reconciler) factory() *virtv2.VirtualMachineBlockDeviceAttachment { + return &virtv2.VirtualMachineBlockDeviceAttachment{} +} + +func (r *Reconciler) statusGetter(obj *virtv2.VirtualMachineBlockDeviceAttachment) virtv2.VirtualMachineBlockDeviceAttachmentStatus { + return obj.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda/vmbda_webhook.go b/images/virtualization-artifact/pkg/controller/vmbda/vmbda_webhook.go new file mode 100644 index 000000000..628eadd7a --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmbda/vmbda_webhook.go @@ -0,0 +1,70 @@ +/* +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 vmbda + +import ( + "context" + "fmt" + "log/slog" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Validator struct { + logger *slog.Logger +} + +func NewValidator() *Validator { + return &Validator{ + logger: slog.Default().With("controller", "vmbda", "webhook", "validator"), + } +} + +func (v *Validator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + err := fmt.Errorf("misconfigured webhook rules: create operation not implemented") + v.logger.Error("Ensure the correctness of ValidatingWebhookConfiguration", "err", err) + return nil, nil +} + +func (v *Validator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + oldVMBDA, ok := oldObj.(*virtv2.VirtualMachineBlockDeviceAttachment) + if !ok { + return nil, fmt.Errorf("expected an old VirtualMachineBlockDeviceAttachment but got a %T", newObj) + } + + newVMBDA, ok := newObj.(*virtv2.VirtualMachineBlockDeviceAttachment) + if !ok { + return nil, fmt.Errorf("expected a new VirtualMachineBlockDeviceAttachment but got a %T", newObj) + } + + v.logger.Info("Validating VirtualMachineBlockDeviceAttachment") + + if oldVMBDA.Generation != newVMBDA.Generation { + return nil, fmt.Errorf("VirtualMachineBlockDeviceAttachment is an idempotent resource: specification changes are not available") + } + + return nil, nil +} + +func (v *Validator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + err := fmt.Errorf("misconfigured webhook rules: delete operation not implemented") + v.logger.Error("Ensure the correctness of ValidatingWebhookConfiguration", "err", err) + return nil, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vmbda_controller.go b/images/virtualization-artifact/pkg/controller/vmbda_controller.go deleted file mode 100644 index 662a4edbd..000000000 --- a/images/virtualization-artifact/pkg/controller/vmbda_controller.go +++ /dev/null @@ -1,88 +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 controller - -import ( - "context" - - "github.com/go-logr/logr" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/metrics" - - vmbdametrics "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/vmbda" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" - "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -const VMBDAControllerName = "vmbda-controller" - -func NewVMBDAController( - ctx context.Context, - mgr manager.Manager, - log logr.Logger, - controllerNamespace string, -) (controller.Controller, error) { - reconciler := NewVMBDAReconciler(controllerNamespace) - - mgrCache := mgr.GetCache() - reconcilerCore := two_phase_reconciler.NewReconcilerCore[*VMBDAReconcilerState]( - reconciler, - NewVMBDAReconcilerState, - two_phase_reconciler.ReconcilerOptions{ - Client: mgr.GetClient(), - Cache: mgrCache, - Recorder: mgr.GetEventRecorderFor(VMBDAControllerName), - Scheme: mgr.GetScheme(), - Log: log.WithName(VMBDAControllerName), - }) - - cvmiController, err := controller.New(VMBDAControllerName, mgr, controller.Options{Reconciler: reconcilerCore}) - if err != nil { - return nil, err - } - if err = reconciler.SetupController(ctx, mgr, cvmiController); err != nil { - return nil, err - } - - if err = builder.WebhookManagedBy(mgr). - For(&v1alpha2.VirtualMachineBlockDeviceAttachment{}). - WithValidator(NewVMBDAValidator(log)). - Complete(); err != nil { - return nil, err - } - - vmbdametrics.SetupCollector(&vmbdaLister{vmbdaCache: mgrCache}, metrics.Registry) - - log.Info("Initialized VirtualMachineBlockDeviceAttachment controller") - return cvmiController, nil -} - -type vmbdaLister struct { - vmbdaCache cache.Cache -} - -func (l vmbdaLister) List() ([]v1alpha2.VirtualMachineBlockDeviceAttachment, error) { - vmbdas := v1alpha2.VirtualMachineBlockDeviceAttachmentList{} - err := l.vmbdaCache.List(context.Background(), &vmbdas) - if err != nil { - return nil, err - } - return vmbdas.Items, nil -} diff --git a/images/virtualization-artifact/pkg/controller/vmbda_reconciler.go b/images/virtualization-artifact/pkg/controller/vmbda_reconciler.go deleted file mode 100644 index 4a0aad23e..000000000 --- a/images/virtualization-artifact/pkg/controller/vmbda_reconciler.go +++ /dev/null @@ -1,371 +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 controller - -import ( - "context" - "fmt" - "strings" - "time" - - corev1 "k8s.io/api/core/v1" - virtv1 "kubevirt.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "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/virtualization-controller/pkg/controller/kubevirt" - "github.com/deckhouse/virtualization-controller/pkg/controller/kvapi" - "github.com/deckhouse/virtualization-controller/pkg/controller/kvbuilder" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type VMBDAReconciler struct { - controllerNamespace string -} - -func NewVMBDAReconciler(controllerNamespace string) *VMBDAReconciler { - return &VMBDAReconciler{ - controllerNamespace: controllerNamespace, - } -} - -func (r *VMBDAReconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { - return ctr.Watch(source.Kind(mgr.GetCache(), &virtv2.VirtualMachineBlockDeviceAttachment{}), &handler.EnqueueRequestForObject{}, - predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { return true }, - DeleteFunc: func(e event.DeleteEvent) bool { return true }, - UpdateFunc: func(e event.UpdateEvent) bool { return true }, - }, - ) -} - -func (r *VMBDAReconciler) Sync(ctx context.Context, _ reconcile.Request, state *VMBDAReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - if state.isDeletion() { - // VM may be deleted before deleting VMBDA or disk may not be attached. - if state.VM != nil && isAttached(state) { - opts.Log.Info("Start volume detaching", "vmbda.name", state.VMBDA.Current().Name) - - err := r.unplugVolume(ctx, state) - if err != nil { - return err - } - - if state.RemoveVMStatusBDA() { - if err := opts.Client.Status().Update(ctx, state.VM); err != nil { - return fmt.Errorf("failed to remove attached disk %s: %w", state.VMD.Name, err) - } - } - opts.Log.Info("Volume detached", "vmbda.name", state.VMBDA.Current().Name, "vm.name", state.VM.Name) - } - - controllerutil.RemoveFinalizer(state.VMBDA.Changed(), virtv2.FinalizerVMBDACleanup) - - return nil - } - - // Set finalizer atomically using requeue. - if controllerutil.AddFinalizer(state.VMBDA.Changed(), virtv2.FinalizerVMBDACleanup) { - state.SetReconcilerResult(&reconcile.Result{Requeue: true}) - return nil - } - - // Do nothing if VM not found or not running. - if state.VM == nil { - opts.Log.V(1).Info(fmt.Sprintf("VM %s is not created, do nothing", state.VMBDA.Current().Spec.VirtualMachine)) - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - state.SetStatusFailure(virtv2.ReasonHotplugPostponed, "VM is missing") - return nil - } - - if state.VM.Status.Phase != virtv2.MachineRunning { - opts.Log.V(1).Info(fmt.Sprintf("VM %s is not running yet, do nothing", state.VMBDA.Current().Spec.VirtualMachine)) - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - state.SetStatusFailure(virtv2.ReasonHotplugPostponed, "VM is not Running") - return nil - } - - // Do nothing if VM not found or not running. - if state.KVVMI == nil { - opts.Log.V(1).Info(fmt.Sprintf("KVVMI for VM %s is absent, do nothing", state.VMBDA.Current().Spec.VirtualMachine)) - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - state.SetStatusFailure(virtv2.ReasonHotplugPostponed, "VM is missing") - return nil - } - - // Do nothing if KVVMI is not running. - if state.KVVMI.Status.Phase != virtv1.Running { - opts.Log.V(1).Info(fmt.Sprintf("KVVMI for VM %s is not running yet, do nothing", state.VMBDA.Current().Spec.VirtualMachine)) - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - state.SetStatusFailure(virtv2.ReasonHotplugPostponed, "VM is not Running") - return nil - } - - // Do nothing if VMD not found or not running. - if state.VMD == nil || state.VMD.Status.Phase != virtv2.DiskReady { - opts.Log.V(1).Info("virtual disk is not ready yet, do nothing") - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - state.SetStatusFailure(virtv2.ReasonHotplugPostponed, "virtual disk is not ready") - return nil - } - - // Do nothing if PVC not found or not running. - if state.PVC == nil || state.PVC.Status.Phase != corev1.ClaimBound { - opts.Log.V(1).Info("PVC is not bound yet, do nothing") - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - state.SetStatusFailure(virtv2.ReasonHotplugPostponed, "PVC is not bound") - return nil - } - - blockDeviceIndex := state.IndexVMStatusBDA() - - // VM is running and disk is valid. Attach volume if not attached yet. - if !isAttached(state) && blockDeviceIndex == -1 { - opts.Log.Info("Start volume attaching") - - // Wait for hotplug possibility. - hotplugMessage, ok := r.checkHotplugSanity(state) - if !ok { - opts.Log.Error(fmt.Errorf("hotplug not possible: %s", hotplugMessage), "") - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - state.SetStatusFailure(virtv2.ReasonHotplugPostponed, hotplugMessage) - return nil - } - - err := r.hotplugVolume(ctx, state) - if err != nil { - return err - } - - // Add attached device to the VM status. - if r.setVMStatusBlockDeviceRefs(blockDeviceIndex, state) { - err = opts.Client.Status().Update(ctx, state.VM) - if err != nil { - return fmt.Errorf("failed to update VM status with hotplugged block device %s: %w", state.VMD.Name, err) - } - } - - opts.Log.Info("Volume attached") - } - - if !isAttached(state) { - // Wait until attached to the KVVMI to update Status.Target. - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - } - - if r.setVMHotpluggedFinalizer(state) { - err := opts.Client.Update(ctx, state.VMD) - if err != nil { - return fmt.Errorf("failed to set virtual disk finalizer with hotplugged block device %s: %w", state.VMD.Name, err) - } - } - - if r.setVMStatusBlockDeviceRefs(blockDeviceIndex, state) { - err := opts.Client.Status().Update(ctx, state.VM) - if err != nil { - return fmt.Errorf("failed to update VM status with hotplugged block device %s: %w", state.VMD.Name, err) - } - } - - return nil -} - -func (r *VMBDAReconciler) UpdateStatus(_ context.Context, _ reconcile.Request, state *VMBDAReconcilerState, _ two_phase_reconciler.ReconcilerOptions) error { - // Do nothing if object is being deleted as any update will lead to en error. - if state.isDeletion() { - return nil - } - - state.VMBDA.Changed().Status.FailureReason = state.FailureReason - state.VMBDA.Changed().Status.FailureMessage = state.FailureMessage - - if state.KVVMI == nil || state.VMD == nil { - state.VMBDA.Changed().Status.Phase = virtv2.BlockDeviceAttachmentPhaseInProgress - return nil - } - - for _, volumeStatus := range state.KVVMI.Status.VolumeStatus { - if volumeStatus.Name != kvbuilder.GenerateVMDDiskName(state.VMD.Name) { - continue - } - - switch volumeStatus.Phase { - case virtv1.VolumeReady: - state.VMBDA.Changed().Status.Phase = virtv2.BlockDeviceAttachmentPhaseAttached - default: - state.VMBDA.Changed().Status.Phase = virtv2.BlockDeviceAttachmentPhaseInProgress - } - - break - } - - return nil -} - -func isAttached(state *VMBDAReconcilerState) bool { - if state.KVVMI == nil || state.VMD == nil { - return false - } - - for _, status := range state.KVVMI.Status.VolumeStatus { - if status.Name == kvbuilder.GenerateVMDDiskName(state.VMD.Name) { - return status.Phase == virtv1.VolumeReady - } - } - - return false -} - -// hotplugVolume requests kubevirt subresources APIService to attach volume to KVVMI. -func (r *VMBDAReconciler) hotplugVolume(ctx context.Context, state *VMBDAReconcilerState) error { - if state.VMBDA.Current().Spec.BlockDeviceRef.Kind != virtv2.VMBDAObjectRefKindVirtualDisk { - return fmt.Errorf("unknown block device attachment kind %s", state.VMBDA.Current().Spec.BlockDeviceRef.Kind) - } - - name := kvbuilder.GenerateVMDDiskName(state.VMBDA.Current().Spec.BlockDeviceRef.Name) - hotplugRequest := virtv1.AddVolumeOptions{ - Name: name, - Disk: &virtv1.Disk{ - Name: name, - DiskDevice: virtv1.DiskDevice{ - Disk: &virtv1.DiskTarget{ - Bus: "scsi", - }, - }, - Serial: state.VMBDA.Current().Spec.BlockDeviceRef.Name, - }, - VolumeSource: &virtv1.HotplugVolumeSource{ - PersistentVolumeClaim: &virtv1.PersistentVolumeClaimVolumeSource{ - PersistentVolumeClaimVolumeSource: corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: state.PVC.Name, - }, - Hotpluggable: true, - }, - }, - } - kv, err := kubevirt.New(ctx, state.Client, r.controllerNamespace) - if err != nil { - return err - } - kvApi := kvapi.New(state.Client, kv) - err = kvApi.AddVolume(ctx, state.VMBDA.Current().Namespace, state.VMBDA.Current().Spec.VirtualMachine, &hotplugRequest) - if err != nil { - return fmt.Errorf("error adding volume, %w", err) - } - - return nil -} - -// unplugVolume requests kubevirt subresources APIService to detach volume from KVVMI. -func (r *VMBDAReconciler) unplugVolume(ctx context.Context, state *VMBDAReconcilerState) error { - if state.VMBDA.Current().Spec.BlockDeviceRef.Kind != virtv2.VMBDAObjectRefKindVirtualDisk { - return fmt.Errorf("unknown block device attachment type %s", state.VMBDA.Current().Spec.BlockDeviceRef.Kind) - } - - name := kvbuilder.GenerateVMDDiskName(state.VMBDA.Current().Spec.BlockDeviceRef.Name) - unplugRequest := virtv1.RemoveVolumeOptions{ - Name: name, - } - - kv, err := kubevirt.New(ctx, state.Client, r.controllerNamespace) - if err != nil { - return err - } - kvApi := kvapi.New(state.Client, kv) - err = kvApi.RemoveVolume(ctx, state.VMBDA.Current().Namespace, state.VMBDA.Current().Spec.VirtualMachine, &unplugRequest) - if err != nil { - return fmt.Errorf("error removing volume, %w", err) - } - - return nil -} - -func (r *VMBDAReconciler) setVMHotpluggedFinalizer(state *VMBDAReconcilerState) bool { - return controllerutil.AddFinalizer(state.VMD, virtv2.FinalizerVDProtection) -} - -// setVMStatusBlockDeviceRefs copy volume status from KVVMI for attached disk to the d8 VM block devices status. -func (r *VMBDAReconciler) setVMStatusBlockDeviceRefs(blockDeviceIndex int, state *VMBDAReconcilerState) bool { - var vs virtv1.VolumeStatus - - for i := range state.KVVMI.Status.VolumeStatus { - if state.KVVMI.Status.VolumeStatus[i].Name == state.VMD.Name { - vs = state.KVVMI.Status.VolumeStatus[i] - } - } - - if blockDeviceIndex > -1 { - blockDevice := state.VM.Status.BlockDeviceRefs[blockDeviceIndex] - if blockDevice.Target != vs.Target || blockDevice.Size != state.VMD.Status.Capacity { - blockDevice.Target = vs.Target - blockDevice.Size = state.VMD.Status.Capacity - - state.VM.Status.BlockDeviceRefs[blockDeviceIndex] = blockDevice - - return true - } - - return false - } - - state.VM.Status.BlockDeviceRefs = append(state.VM.Status.BlockDeviceRefs, virtv2.BlockDeviceStatusRef{ - Kind: virtv2.DiskDevice, - Name: state.VMD.Name, - Target: vs.Target, - Size: state.VMD.Status.Capacity, - Hotpluggable: true, - }) - - return true -} - -// checkHotplugSanity detects if it is possible to hotplug disk to the VM. -// 1. It searches for disk in VM spec and returns false if disk is already attached to VM. -// 2. It returns false if VM is in the "Manual approve" mode. -func (r *VMBDAReconciler) checkHotplugSanity(state *VMBDAReconcilerState) (string, bool) { - if state.VM == nil { - return "", true - } - - var messages []string - - // Check if disk is already in the spec of VM. - diskName := state.VMBDA.Current().Spec.BlockDeviceRef.Name - - for _, bd := range state.VM.Spec.BlockDeviceRefs { - if bd.Kind == virtv2.DiskDevice && bd.Name == diskName { - messages = append(messages, fmt.Sprintf("disk %s is already attached to virtual machine", diskName)) - break - } - } - if len(state.VM.Status.RestartAwaitingChanges) > 0 { - messages = append(messages, "virtual machine waits for restart approval") - } - - if len(messages) == 0 { - return "", true - } - - return strings.Join(messages, ", "), false -} diff --git a/images/virtualization-artifact/pkg/controller/vmbda_reconciler_state.go b/images/virtualization-artifact/pkg/controller/vmbda_reconciler_state.go deleted file mode 100644 index a94948d94..000000000 --- a/images/virtualization-artifact/pkg/controller/vmbda_reconciler_state.go +++ /dev/null @@ -1,176 +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 controller - -import ( - "context" - "fmt" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - virtv1 "kubevirt.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type VMBDAReconcilerState struct { - Client client.Client - VMBDA *helper.Resource[*virtv2.VirtualMachineBlockDeviceAttachment, virtv2.VirtualMachineBlockDeviceAttachmentStatus] - VM *virtv2.VirtualMachine - KVVMI *virtv1.VirtualMachineInstance - - VMD *virtv2.VirtualDisk - PVC *corev1.PersistentVolumeClaim - - Result *reconcile.Result - - FailureReason string - FailureMessage string -} - -func NewVMBDAReconcilerState(name types.NamespacedName, log logr.Logger, client client.Client, cache cache.Cache) *VMBDAReconcilerState { - state := &VMBDAReconcilerState{ - Client: client, - VMBDA: helper.NewResource( - name, log, client, cache, - func() *virtv2.VirtualMachineBlockDeviceAttachment { - return &virtv2.VirtualMachineBlockDeviceAttachment{} - }, - func(obj *virtv2.VirtualMachineBlockDeviceAttachment) virtv2.VirtualMachineBlockDeviceAttachmentStatus { - return obj.Status - }, - ), - } - - return state -} - -func (state *VMBDAReconcilerState) ApplySync(ctx context.Context, _ logr.Logger) error { - if err := state.VMBDA.UpdateMeta(ctx); err != nil { - return fmt.Errorf("unable to update VMBDA %q meta: %w", state.VMBDA.Name(), err) - } - return nil -} - -func (state *VMBDAReconcilerState) ApplyUpdateStatus(ctx context.Context, _ logr.Logger) error { - return state.VMBDA.UpdateStatus(ctx) -} - -func (state *VMBDAReconcilerState) SetReconcilerResult(result *reconcile.Result) { - state.Result = result -} - -func (state *VMBDAReconcilerState) GetReconcilerResult() *reconcile.Result { - return state.Result -} - -func (state *VMBDAReconcilerState) SetStatusFailure(reason, message string) { - state.FailureReason = reason - state.FailureMessage = message -} - -func (state *VMBDAReconcilerState) Reload(ctx context.Context, req reconcile.Request, log logr.Logger, client client.Client) error { - err := state.VMBDA.Fetch(ctx) - if err != nil { - return fmt.Errorf("unable to get VMBDA %s: %w", req.NamespacedName, err) - } - - if state.VMBDA.IsEmpty() { - log.Info("Reconcile observe an absent VMBDA: it may be deleted", "vmbda.name", req.NamespacedName) - return nil - } - - vmKey := types.NamespacedName{Name: state.VMBDA.Current().Spec.VirtualMachine, Namespace: state.VMBDA.Current().Namespace} - state.VM, err = helper.FetchObject(ctx, vmKey, client, &virtv2.VirtualMachine{}) - if err != nil { - return fmt.Errorf("unable to get VM %s: %w", vmKey, err) - } - - kvvmiKey := types.NamespacedName{Name: state.VMBDA.Current().Spec.VirtualMachine, Namespace: state.VMBDA.Current().Namespace} - state.KVVMI, err = helper.FetchObject(ctx, kvvmiKey, client, &virtv1.VirtualMachineInstance{}) - if err != nil { - return fmt.Errorf("unable to get KVVMI %s: %w", kvvmiKey, err) - } - - switch state.VMBDA.Current().Spec.BlockDeviceRef.Kind { - case virtv2.VMBDAObjectRefKindVirtualDisk: - vmdKey := types.NamespacedName{Name: state.VMBDA.Current().Spec.BlockDeviceRef.Name, Namespace: state.VMBDA.Current().Namespace} - state.VMD, err = helper.FetchObject(ctx, vmdKey, client, &virtv2.VirtualDisk{}) - if err != nil { - return fmt.Errorf("unable to get virtual disk %s: %w", vmdKey, err) - } - - if state.VMD == nil { - return nil - } - - pvcKey := types.NamespacedName{Name: state.VMD.Status.Target.PersistentVolumeClaim, Namespace: state.VMBDA.Current().Namespace} - state.PVC, err = helper.FetchObject(ctx, pvcKey, client, &corev1.PersistentVolumeClaim{}) - if err != nil { - return fmt.Errorf("unable to get PVC %s: %w", pvcKey, err) - } - default: - return fmt.Errorf("unknown block device attachment type %s", state.VMBDA.Current().Spec.BlockDeviceRef.Kind) - } - - return nil -} - -func (state *VMBDAReconcilerState) ShouldReconcile(_ logr.Logger) bool { - return !state.VMBDA.IsEmpty() -} - -func (state *VMBDAReconcilerState) isDeletion() bool { - return state.VMBDA.Current().DeletionTimestamp != nil -} - -func (state *VMBDAReconcilerState) IndexVMStatusBDA() int { - if state.VM == nil || state.VMD == nil { - return -1 - } - - for i, bda := range state.VM.Status.BlockDeviceRefs { - if bda.Kind == virtv2.DiskDevice && bda.Name == state.VMD.Name { - return i - } - } - return -1 -} - -// RemoveVMStatusBDA removes device from VM.Status.BlockDeviceRefs by its name. -func (state *VMBDAReconcilerState) RemoveVMStatusBDA() bool { - if state.VM == nil { - return false - } - - blockDeviceIndex := state.IndexVMStatusBDA() - if blockDeviceIndex == -1 { - return false - } - - state.VM.Status.BlockDeviceRefs = append( - state.VM.Status.BlockDeviceRefs[:blockDeviceIndex], - state.VM.Status.BlockDeviceRefs[blockDeviceIndex+1:]..., - ) - - return true -} diff --git a/images/virtualization-artifact/pkg/controller/vmbda_webhook.go b/images/virtualization-artifact/pkg/controller/vmbda_webhook.go deleted file mode 100644 index 0421da471..000000000 --- a/images/virtualization-artifact/pkg/controller/vmbda_webhook.go +++ /dev/null @@ -1,96 +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 controller - -import ( - "context" - "errors" - "fmt" - - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -func NewVMBDAValidator(log logr.Logger) *VMBDAValidator { - return &VMBDAValidator{log: log} -} - -type VMBDAValidator struct { - log logr.Logger -} - -func (v *VMBDAValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { - vmbda, ok := obj.(*v1alpha2.VirtualMachineBlockDeviceAttachment) - if !ok { - return nil, fmt.Errorf("expected a new VirtualMachineBlockDeviceAttachment but got a %T", obj) - } - - v.log.Info("Validating VMBDA") - - switch vmbda.Spec.BlockDeviceRef.Kind { - case v1alpha2.VMBDAObjectRefKindVirtualDisk: - if vmbda.Spec.BlockDeviceRef.Name == "" { - return nil, errors.New("virtual disk name is omitted, but required") - } - default: - return nil, fmt.Errorf("unknown block device kind %q", vmbda.Spec.BlockDeviceRef.Kind) - } - - return nil, nil -} - -func (v *VMBDAValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - newVMBDA, ok := newObj.(*v1alpha2.VirtualMachineBlockDeviceAttachment) - if !ok { - return nil, fmt.Errorf("expected a new VirtualMachineBlockDeviceAttachment but got a %T", newObj) - } - - oldVMBDA, ok := oldObj.(*v1alpha2.VirtualMachineBlockDeviceAttachment) - if !ok { - return nil, fmt.Errorf("expected an old VirtualMachineBlockDeviceAttachment but got a %T", oldObj) - } - - v.log.Info("Validating VMBDA") - - if newVMBDA.Spec.VirtualMachine != oldVMBDA.Spec.VirtualMachine { - return nil, errors.New("virtual machine name cannot be changed once set") - } - - if newVMBDA.Spec.BlockDeviceRef.Kind != oldVMBDA.Spec.BlockDeviceRef.Kind { - return nil, errors.New("block device type cannot be changed once set") - } - - switch newVMBDA.Spec.BlockDeviceRef.Kind { - case v1alpha2.VMBDAObjectRefKindVirtualDisk: - if newVMBDA.Spec.BlockDeviceRef.Name != oldVMBDA.Spec.BlockDeviceRef.Name { - return nil, errors.New("virtual disk name cannot be changed once set") - } - default: - return nil, fmt.Errorf("unknown block device kind %q", newVMBDA.Spec.BlockDeviceRef.Kind) - } - - return nil, nil -} - -func (v *VMBDAValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { - err := fmt.Errorf("misconfigured webhook rules: delete operation not implemented") - v.log.Error(err, "Ensure the correctness of ValidatingWebhookConfiguration") - return nil, nil -} diff --git a/images/virtualization-artifact/pkg/monitoring/metrics/vmbda/collector.go b/images/virtualization-artifact/pkg/monitoring/metrics/vmbda/collector.go index 74f904e86..76889ec12 100644 --- a/images/virtualization-artifact/pkg/monitoring/metrics/vmbda/collector.go +++ b/images/virtualization-artifact/pkg/monitoring/metrics/vmbda/collector.go @@ -89,15 +89,17 @@ func (s *scraper) Report(vmbdas []virtv2.VirtualMachineBlockDeviceAttachment) { func (s *scraper) updateVMBDAStatusPhaseMetrics(vmbda virtv2.VirtualMachineBlockDeviceAttachment) { phase := vmbda.Status.Phase if phase == "" { - phase = virtv2.BlockDeviceAttachmentPhaseInProgress + phase = virtv2.BlockDeviceAttachmentPhasePending } phases := []struct { value bool name string }{ + {phase == virtv2.BlockDeviceAttachmentPhasePending, string(virtv2.BlockDeviceAttachmentPhasePending)}, {phase == virtv2.BlockDeviceAttachmentPhaseInProgress, string(virtv2.BlockDeviceAttachmentPhaseInProgress)}, {phase == virtv2.BlockDeviceAttachmentPhaseAttached, string(virtv2.BlockDeviceAttachmentPhaseAttached)}, {phase == virtv2.BlockDeviceAttachmentPhaseFailed, string(virtv2.BlockDeviceAttachmentPhaseFailed)}, + {phase == virtv2.BlockDeviceAttachmentPhaseTerminating, string(virtv2.BlockDeviceAttachmentPhaseTerminating)}, } desc := vmbdaMetrics[MetricVMBDAStatusPhase] for _, p := range phases { diff --git a/templates/virtualization-controller/validation-webhook.yaml b/templates/virtualization-controller/validation-webhook.yaml index cbf071aa4..4bf9e8eaf 100644 --- a/templates/virtualization-controller/validation-webhook.yaml +++ b/templates/virtualization-controller/validation-webhook.yaml @@ -58,7 +58,7 @@ webhooks: rules: - apiGroups: ["virtualization.deckhouse.io"] apiVersions: ["v1alpha2"] - operations: ["CREATE", "UPDATE"] + operations: ["UPDATE"] resources: ["virtualmachineblockdeviceattachments"] scope: "Namespaced" clientConfig: