diff --git a/api/core/v1alpha2/finalizers.go b/api/core/v1alpha2/finalizers.go index ffa513ce2..d8ba88b84 100644 --- a/api/core/v1alpha2/finalizers.go +++ b/api/core/v1alpha2/finalizers.go @@ -18,6 +18,7 @@ package v1alpha2 const ( FinalizerClusterVirtualImageProtection = "virtualization.deckhouse.io/cvi-protection" + FinalizerVirtualImageProtection = "virtualization.deckhouse.io/vi-protection" FinalizerVirtualDiskProtection = "virtualization.deckhouse.io/vd-protection" FinalizerPodProtection = "virtualization.deckhouse.io/pod-protection" FinalizerServiceProtection = "virtualization.deckhouse.io/svc-protection" @@ -37,7 +38,7 @@ const ( FinalizerCVICleanup = "virtualization.deckhouse.io/cvi-cleanup" FinalizerVDCleanup = "virtualization.deckhouse.io/vd-cleanup" - FinalizerVMICleanup = "virtualization.deckhouse.io/vi-cleanup" + FinalizerVICleanup = "virtualization.deckhouse.io/vi-cleanup" FinalizerVMCleanup = "virtualization.deckhouse.io/vm-cleanup" FinalizerIPAddressClaimCleanup = "virtualization.deckhouse.io/vmip-cleanup" FinalizerIPAddressLeaseCleanup = "virtualization.deckhouse.io/vmipl-cleanup" diff --git a/api/core/v1alpha2/image_status.go b/api/core/v1alpha2/image_status.go index 33c9c9cc3..bc35efc1f 100644 --- a/api/core/v1alpha2/image_status.go +++ b/api/core/v1alpha2/image_status.go @@ -24,8 +24,6 @@ const ( ImageProvisioning ImagePhase = "Provisioning" ImageReady ImagePhase = "Ready" ImageFailed ImagePhase = "Failed" - ImagePVCLost ImagePhase = "PVCLost" - ImageUnknown ImagePhase = "Unknown" ImageTerminating ImagePhase = "Terminating" ) @@ -40,9 +38,6 @@ type ImageStatus struct { Phase ImagePhase `json:"phase,omitempty"` Progress string `json:"progress,omitempty"` UploadCommand string `json:"uploadCommand,omitempty"` - // TODO remove. - FailureReason string `json:"failureReason,omitempty"` - FailureMessage string `json:"failureMessage,omitempty"` } type StatusSpeed struct { diff --git a/api/core/v1alpha2/vicondition/condition.go b/api/core/v1alpha2/vicondition/condition.go new file mode 100644 index 000000000..ff546860d --- /dev/null +++ b/api/core/v1alpha2/vicondition/condition.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 vicondition + +type Type = string + +const ( + DatasourceReadyType Type = "DatasourceReady" + ReadyType Type = "Ready" +) + +type ( + DatasourceReadyReason = string + ReadyReason = string +) + +const ( + DatasourceReadyReason_DatasourceReady DatasourceReadyReason = "DatasourceReady" + DatasourceReadyReason_ContainerRegistrySecretNotFound DatasourceReadyReason = "ContainerRegistrySecretNotFound" + DatasourceReadyReason_ImageNotReady DatasourceReadyReason = "ImageNotReady" + DatasourceReadyReason_ClusterImageNotReady DatasourceReadyReason = "ClusterImageNotReady" + + ReadyReason_WaitForUserUpload ReadyReason = "WaitForUserUpload" + ReadyReason_Provisioning ReadyReason = "Provisioning" + ReadyReason_ProvisioningNotStarted ReadyReason = "ProvisioningNotStarted" + ReadyReason_ProvisioningFailed ReadyReason = "ProvisioningFailed" + ReadyReason_Ready ReadyReason = "Ready" +) diff --git a/api/core/v1alpha2/virtual_image.go b/api/core/v1alpha2/virtual_image.go index d7eb26563..5883718ab 100644 --- a/api/core/v1alpha2/virtual_image.go +++ b/api/core/v1alpha2/virtual_image.go @@ -56,7 +56,9 @@ type VirtualImageSpec struct { } type VirtualImageStatus struct { - ImageStatus `json:",inline"` + ImageStatus `json:",inline"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } type VirtualImageDataSource struct { diff --git a/crds/doc-ru-virtualimage.yaml b/crds/doc-ru-virtualimage.yaml index 0e6af2da9..527292330 100644 --- a/crds/doc-ru-virtualimage.yaml +++ b/crds/doc-ru-virtualimage.yaml @@ -51,9 +51,6 @@ spec: description: "" sha256: description: "" - insecureSkipVerify: - description: | - Отключить проверку TLS-сертификата (небезопасно и не должно использоваться в производственных средах). url: description: | URL с образом. Поддерживаются следующие типы образов: @@ -92,9 +89,22 @@ spec: * `ContainerRegistry` — использовать container registry (DVCR). В этом случае образы будут загружаться в контейнер, а затем в DVCR (поставляется с модулем виртуализации). status: properties: - attachedToVirtualMachines: + conditions: description: | - Список виртуальных машин, использующих этот образ + Последние доступные наблюдения текущего состояния объекта. + properties: + type: + description: Тип состояния. + status: + description: Статус состояния (одно из True, False, Unknown). + message: + description: Cообщение c деталями последнего перехода состояния. + reason: + description: Краткая причина последнего перехода состояния. + lastProbeTime: + description: Время последней проверки состояния. + lastTransitionTime: + description: Время последнего перехода состояния из одного статуса в другой. cdrom: description: | Является ли образ форматом, который должен быть смонтирован как cdrom, например iso и т. д. diff --git a/crds/virtualimage.yaml b/crds/virtualimage.yaml index e27678da7..a8c6ba643 100644 --- a/crds/virtualimage.yaml +++ b/crds/virtualimage.yaml @@ -103,12 +103,6 @@ spec: description: The CA chain in base64 format to verify the url. example: | YWFhCg== - insecureSkipVerify: - type: boolean - default: false - description: | - If a CA chain isn't provided, this option can be used to turn off TLS certificate checks. - As noted, it is insecure and shouldn't be used in production environments. checksum: type: object description: | @@ -198,6 +192,41 @@ spec: status: type: object properties: + conditions: + type: array + description: | + The latest available observations of an object's current state. + 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 + description: Last time the condition transit from one status to another. + format: date-time + required: + - type + - status downloadSpeed: type: object description: | @@ -273,9 +302,8 @@ spec: * Provisioning - The process of resource creation (copying/downloading/building the image) is in progress. * 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. - * Failed - There was a problem when creating a resource, details can be seen in `.status.failureReason` and `.status.failureMessage`. - * NotReady - It is not possible to get information about the child image because of inability to connect to DVCR. The resource cannot be used. - * ImageLost - The child image of the resource is missing. The resource cannot be used. + * Failed - There was a problem when creating a resource. + * Terminating - The process of resource deletion is in progress. enum: [ @@ -284,8 +312,7 @@ spec: "WaitForUserUpload", "Ready", "Failed", - "ImageLost", - "Unknown", + "Terminating", ] progress: type: string @@ -295,24 +322,10 @@ spec: type: string description: | Command for uploading a image for the 'Upload' type. - failureReason: - type: string - description: | - A brief description of the cause of the error. - failureMessage: - type: string - description: | - Detailed description of the error. - attachedToVirtualMachines: - type: array + observedGeneration: + type: integer description: | - A list of `VirtualMachines` that use the disk - example: [{ name: "VM100" }, { name: "VM200" }] - items: - type: object - properties: - name: - type: string + Represents the .metadata.generation that the status was set based upon. 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 1ab932f0e..439bc70b8 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -45,6 +45,7 @@ import ( "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/vmop" virtv2alpha1 "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -187,7 +188,7 @@ func main() { os.Exit(1) } - if _, err := controller.NewVMIController(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) } diff --git a/images/virtualization-artifact/pkg/common/vmi/util.go b/images/virtualization-artifact/pkg/common/vmi/util.go deleted file mode 100644 index abd7cce6e..000000000 --- a/images/virtualization-artifact/pkg/common/vmi/util.go +++ /dev/null @@ -1,66 +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 vmi - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - virtv2alpha1 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -// MakeOwnerReference makes owner reference from a ClusterVirtualImage. -func MakeOwnerReference(vmi *virtv2alpha1.VirtualImage) metav1.OwnerReference { - blockOwnerDeletion := true - isController := true - return metav1.OwnerReference{ - APIVersion: virtv2alpha1.VirtualImageGVK.GroupVersion().String(), - Kind: virtv2alpha1.VirtualImageGVK.Kind, - Name: vmi.Name, - UID: vmi.GetUID(), - BlockOwnerDeletion: &blockOwnerDeletion, - Controller: &isController, - } -} - -func GetDataSourceType(vmi *virtv2alpha1.VirtualImage) string { - if vmi == nil { - return "" - } - return string(vmi.Spec.DataSource.Type) -} - -func IsDVCRSource(vmi *virtv2alpha1.VirtualImage) bool { - return vmi != nil && vmi.Spec.DataSource.Type == virtv2alpha1.DataSourceTypeObjectRef -} - -// IsTwoPhaseImport returns true when two phase import is required: -// 1. Import from dataSource to DVCR image using dvcr-importer or dvcr-uploader. -// 2. Import DVCR image to PVC using DataVolume. -func IsTwoPhaseImport(vmi *virtv2alpha1.VirtualImage) bool { - if vmi == nil { - return false - } - - switch vmi.Spec.DataSource.Type { - case virtv2alpha1.DataSourceTypeHTTP, - virtv2alpha1.DataSourceTypeUpload, - virtv2alpha1.DataSourceTypeContainerImage: - return vmi.Spec.Storage == virtv2alpha1.StorageKubernetes - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/importer/importer_pod.go b/images/virtualization-artifact/pkg/controller/importer/importer_pod.go index 20ed812a5..efe25d3d8 100644 --- a/images/virtualization-artifact/pkg/controller/importer/importer_pod.go +++ b/images/virtualization-artifact/pkg/controller/importer/importer_pod.go @@ -25,6 +25,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" common "github.com/deckhouse/virtualization-controller/pkg/common" @@ -388,3 +389,8 @@ type PodNamer interface { func FindPod(ctx context.Context, client client.Client, name PodNamer) (*corev1.Pod, error) { return helper.FetchObject(ctx, name.ImporterPod(), client, &corev1.Pod{}) } + +func DeletePod(ctx context.Context, clientset kubernetes.Interface, name PodNamer) error { + key := name.ImporterPod() + return clientset.CoreV1().Pods(key.Namespace).Delete(ctx, key.Name, metav1.DeleteOptions{}) +} diff --git a/images/virtualization-artifact/pkg/controller/uploader/uploader_ingress.go b/images/virtualization-artifact/pkg/controller/uploader/uploader_ingress.go index cd101c5fe..374292a20 100644 --- a/images/virtualization-artifact/pkg/controller/uploader/uploader_ingress.go +++ b/images/virtualization-artifact/pkg/controller/uploader/uploader_ingress.go @@ -63,14 +63,6 @@ func (i *Ingress) Create(ctx context.Context, client client.Client) (*netv1.Ingr return ing, nil } -func CleanupIngress(ctx context.Context, client client.Client, ing *netv1.Ingress) error { - if ing == nil { - return nil - } - - return helper.CleanupObject(ctx, client, ing) -} - func (i *Ingress) makeSpec() *netv1.Ingress { pathTypeExact := netv1.PathTypeExact path := i.generatePath() diff --git a/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go b/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go index ae6e3db1c..82477528f 100644 --- a/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go +++ b/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go @@ -78,14 +78,6 @@ func (p *Pod) Create(ctx context.Context, client client.Client) (*corev1.Pod, er return pod, nil } -func CleanupPod(ctx context.Context, client client.Client, pod *corev1.Pod) error { - if pod == nil { - return nil - } - - return helper.CleanupObject(ctx, client, pod) -} - func (p *Pod) makeSpec() *corev1.Pod { pod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{ diff --git a/images/virtualization-artifact/pkg/controller/uploader/uploader_service.go b/images/virtualization-artifact/pkg/controller/uploader/uploader_service.go index b39f5159d..1b7cc85b0 100644 --- a/images/virtualization-artifact/pkg/controller/uploader/uploader_service.go +++ b/images/virtualization-artifact/pkg/controller/uploader/uploader_service.go @@ -53,14 +53,6 @@ func (s *Service) Create(ctx context.Context, client client.Client) (*corev1.Ser return service, nil } -func CleanupService(ctx context.Context, client client.Client, service *corev1.Service) error { - if service == nil { - return nil - } - - return helper.CleanupObject(ctx, client, service) -} - func (s *Service) makeSpec() *corev1.Service { service := &corev1.Service{ TypeMeta: metav1.TypeMeta{ diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/datasource_ready.go b/images/virtualization-artifact/pkg/controller/vi/internal/datasource_ready.go new file mode 100644 index 000000000..44ffb6671 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/datasource_ready.go @@ -0,0 +1,87 @@ +/* +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" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type DatasourceReadyHandler struct { + sources *source.Sources +} + +func NewDatasourceReadyHandler(sources *source.Sources) *DatasourceReadyHandler { + return &DatasourceReadyHandler{ + sources: sources, + } +} + +func (h DatasourceReadyHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + condition, ok := service.GetCondition(vicondition.DatasourceReadyType, vi.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vicondition.DatasourceReadyType, + Status: metav1.ConditionUnknown, + } + } + + defer func() { service.SetCondition(condition, &vi.Status.Conditions) }() + + if vi.DeletionTimestamp != nil { + return reconcile.Result{}, nil + } + + s, ok := h.sources.For(vi.Spec.DataSource.Type) + if !ok { + err := fmt.Errorf("data source validator not found for type: %s", vi.Spec.DataSource.Type) + condition.Message = err.Error() + return reconcile.Result{}, err + } + + err := s.Validate(ctx, vi) + switch { + case err == nil: + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.DatasourceReadyReason_DatasourceReady + condition.Message = "" + case errors.Is(err, source.ErrSecretNotFound): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.DatasourceReadyReason_ContainerRegistrySecretNotFound + condition.Message = strings.ToTitle(err.Error()) + case errors.As(err, &source.ImageNotReadyError{}): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.DatasourceReadyReason_ImageNotReady + condition.Message = strings.ToTitle(err.Error()) + } + + return reconcile.Result{}, err +} + +func (h DatasourceReadyHandler) Name() string { + return "DatasourceReadyHandler" +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go b/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go new file mode 100644 index 000000000..695214970 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/deletion.go @@ -0,0 +1,60 @@ +/* +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" + + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type DeletionHandler struct { + sources *source.Sources +} + +func NewDeletionHandler(sources *source.Sources) *DeletionHandler { + return &DeletionHandler{ + sources: sources, + } +} + +func (h DeletionHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + if vi.DeletionTimestamp != nil { + requeue, err := h.sources.CleanUp(ctx, vi) + if err != nil { + return reconcile.Result{}, err + } + + if requeue { + return reconcile.Result{Requeue: true}, nil + } + + controllerutil.RemoveFinalizer(vi, virtv2.FinalizerVICleanup) + return reconcile.Result{}, nil + } + + controllerutil.AddFinalizer(vi, virtv2.FinalizerVICleanup) + return reconcile.Result{}, nil +} + +func (h DeletionHandler) Name() string { + return "DeletionHandler" +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.go new file mode 100644 index 000000000..85c8871ec --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/life_cycle.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 internal + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type LifeCycleHandler struct { + client client.Client + sources *source.Sources +} + +func NewLifeCycleHandler(sources *source.Sources, client client.Client) *LifeCycleHandler { + return &LifeCycleHandler{ + client: client, + sources: sources, + } +} + +func (h LifeCycleHandler) Handle(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) { + readyCondition, ok := service.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + if !ok { + readyCondition = metav1.Condition{ + Type: vicondition.ReadyType, + Status: metav1.ConditionUnknown, + } + + service.SetCondition(readyCondition, &vi.Status.Conditions) + } + + if vi.DeletionTimestamp != nil { + vi.Status.Phase = virtv2.ImageTerminating + return reconcile.Result{}, nil + } + + if vi.Status.Phase == "" { + vi.Status.Phase = virtv2.ImagePending + } + + dataSourceReadyCondition, exists := service.GetCondition(vicondition.DatasourceReadyType, vi.Status.Conditions) + if !exists { + return reconcile.Result{}, fmt.Errorf("condition %s not found, but required", vicondition.DatasourceReadyType) + } + + if dataSourceReadyCondition.Status != metav1.ConditionTrue { + return reconcile.Result{}, nil + } + + if readyCondition.Status != metav1.ConditionTrue && h.sources.Changed(ctx, vi) { + vi.Status = virtv2.VirtualImageStatus{ + ImageStatus: virtv2.ImageStatus{ + Phase: virtv2.ImagePending, + }, + Conditions: vi.Status.Conditions, + ObservedGeneration: vi.Status.ObservedGeneration, + } + + _, err := h.sources.CleanUp(ctx, vi) + if err != nil { + return reconcile.Result{}, err + } + + return reconcile.Result{Requeue: true}, nil + } + + ds, exists := h.sources.For(vi.Spec.DataSource.Type) + if !exists { + return reconcile.Result{}, fmt.Errorf("data source runner not found for type: %s", vi.Spec.DataSource.Type) + } + + requeue, err := ds.Sync(ctx, vi) + if err != nil { + return reconcile.Result{}, err + } + + return reconcile.Result{Requeue: requeue}, nil +} + +func (h LifeCycleHandler) Name() string { + return "LifeCycleHandler" +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/errors.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/errors.go new file mode 100644 index 000000000..60c631bd0 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/errors.go @@ -0,0 +1,52 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "errors" + "fmt" +) + +var ErrSecretNotFound = errors.New("container registry secret not found") + +type ImageNotReadyError struct { + name string +} + +func (e ImageNotReadyError) Error() string { + return fmt.Sprintf("VirtualImage %s not ready", e.name) +} + +func NewImageNotReadyError(name string) error { + return ImageNotReadyError{ + name: name, + } +} + +type ClusterImageNotReadyError struct { + name string +} + +func (e ClusterImageNotReadyError) Error() string { + return fmt.Sprintf("ClusterVirtualImage %s not ready", e.name) +} + +func NewClusterImageNotReadyError(name string) error { + return ClusterImageNotReadyError{ + name: name, + } +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go new file mode 100644 index 000000000..c705dd054 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/http.go @@ -0,0 +1,188 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "errors" + "log/slog" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + cc "github.com/deckhouse/virtualization-controller/pkg/controller/common" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/util" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type HTTPDataSource struct { + statService Stat + importerService Importer + dvcrSettings *dvcr.Settings + logger *slog.Logger +} + +func NewHTTPDataSource( + statService Stat, + importerService Importer, + dvcrSettings *dvcr.Settings, +) *HTTPDataSource { + return &HTTPDataSource{ + statService: statService, + importerService: importerService, + dvcrSettings: dvcrSettings, + logger: slog.Default().With("controller", util.ControllerShortName, "ds", "http"), + } +} + +func (ds HTTPDataSource) Sync(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + ds.logger.Info("Sync", "vi", vi.Name) + + condition, _ := service.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + defer func() { service.SetCondition(condition, &vi.Status.Conditions) }() + + supgen := supplements.NewGenerator(util.ControllerShortName, vi.Name, vi.Namespace, vi.UID) + pod, err := ds.importerService.GetPod(ctx, supgen) + if err != nil { + return false, err + } + + switch { + case isDiskProvisioningFinished(condition): + ds.logger.Info("Finishing...", "vi", vi.Name) + + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.ReadyReason_Ready + condition.Message = "" + + vi.Status.Phase = virtv2.ImageReady + + err = ds.importerService.Unprotect(ctx, pod) + if err != nil { + return false, err + } + + return CleanUp(ctx, vi, ds) + case cc.IsTerminating(pod): + vi.Status.Phase = virtv2.ImagePending + + ds.logger.Info("Cleaning up...", "vi", vi.Name) + case pod == nil: + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_Provisioning + condition.Message = "DVCR Provisioner not found: create the new one." + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) + vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + envSettings := ds.getEnvSettings(vi, supgen) + err = ds.importerService.Start(ctx, envSettings, vi, supgen, datasource.NewCABundleForVMI(vi.Spec.DataSource)) + if err != nil { + return false, err + } + + ds.logger.Info("Create importer pod...", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", "nil") + case cc.IsPodComplete(pod): + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.ReadyReason_Ready + condition.Message = "" + + vi.Status.Phase = virtv2.ImageReady + vi.Status.Size = ds.statService.GetSize(pod) + vi.Status.CDROM = ds.statService.GetCDROM(pod) + vi.Status.Format = ds.statService.GetFormat(pod) + vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) + vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + ds.logger.Info("Ready", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + default: + err = ds.statService.CheckPod(pod) + if err != nil { + vi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_ProvisioningNotStarted + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + case errors.Is(err, service.ErrProvisioningFailed): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_ProvisioningFailed + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + default: + return false, err + } + } + + err = ds.importerService.Protect(ctx, pod) + if err != nil { + return false, err + } + + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_Provisioning + condition.Message = "Import is in the process of provisioning to DVCR." + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) + vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + ds.logger.Info("Provisioning...", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + } + + return true, nil +} + +func (ds HTTPDataSource) CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + supgen := supplements.NewGenerator(util.ControllerShortName, vi.Name, vi.Namespace, vi.UID) + + requeue, err := ds.importerService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + + return requeue, nil +} + +func (ds HTTPDataSource) Validate(_ context.Context, _ *virtv2.VirtualImage) error { + return nil +} + +func (ds HTTPDataSource) getEnvSettings(vi *virtv2.VirtualImage, supgen *supplements.Generator) *importer.Settings { + var settings importer.Settings + + importer.ApplyHTTPSourceSettings(&settings, vi.Spec.DataSource.HTTP, supgen) + importer.ApplyDVCRDestinationSettings( + &settings, + ds.dvcrSettings, + supgen, + ds.dvcrSettings.RegistryImageForVMI(vi.Name, vi.Namespace), + ) + + return &settings +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go new file mode 100644 index 000000000..17d8f7865 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/interfaces.go @@ -0,0 +1,63 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +//go:generate moq -rm -out mock.go . Importer Uploader Stat + +type Importer interface { + Start(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error + CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) + GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) + Protect(ctx context.Context, pod *corev1.Pod) error + Unprotect(ctx context.Context, pod *corev1.Pod) error +} + +type Uploader interface { + Start(ctx context.Context, settings *uploader.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error + CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) + GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) + GetIngress(ctx context.Context, sup *supplements.Generator) (*netv1.Ingress, error) + GetService(ctx context.Context, sup *supplements.Generator) (*corev1.Service, error) + Protect(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error + Unprotect(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error +} + +type Stat interface { + GetFormat(pod *corev1.Pod) string + GetCDROM(pod *corev1.Pod) bool + GetSize(pod *corev1.Pod) virtv2.ImageStatusSize + GetReasonError(pod *corev1.Pod) (string, string, error) + GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) virtv2.ImageStatusSpeed + GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + IsUploadStarted(ownerUID types.UID, pod *corev1.Pod) bool + CheckPod(pod *corev1.Pod) error +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go new file mode 100644 index 000000000..5c4bf15ff --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref.go @@ -0,0 +1,223 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "errors" + "fmt" + "log/slog" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/controller" + cc "github.com/deckhouse/virtualization-controller/pkg/controller/common" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/util" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type ObjectRefDataSource struct { + statService Stat + importerService Importer + dvcrSettings *dvcr.Settings + client client.Client + logger *slog.Logger +} + +func NewObjectRefDataSource( + statService Stat, + importerService Importer, + dvcrSettings *dvcr.Settings, + client client.Client, +) *ObjectRefDataSource { + return &ObjectRefDataSource{ + statService: statService, + importerService: importerService, + dvcrSettings: dvcrSettings, + client: client, + logger: slog.Default().With("controller", util.ControllerShortName, "ds", "objectref"), + } +} + +func (ds ObjectRefDataSource) Sync(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + ds.logger.Info("Sync", "vi", vi.Name) + + condition, _ := service.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + defer func() { service.SetCondition(condition, &vi.Status.Conditions) }() + + supgen := supplements.NewGenerator(util.ControllerShortName, vi.Name, vi.Namespace, vi.UID) + pod, err := ds.importerService.GetPod(ctx, supgen) + if err != nil { + return false, err + } + + switch { + case isDiskProvisioningFinished(condition): + ds.logger.Info("Finishing...", "vi", vi.Name) + + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.ReadyReason_Ready + condition.Message = "" + + vi.Status.Phase = virtv2.ImageReady + + err = ds.importerService.Unprotect(ctx, pod) + if err != nil { + return false, err + } + + return CleanUp(ctx, vi, ds) + case cc.IsTerminating(pod): + + vi.Status.Phase = virtv2.ImagePending + + ds.logger.Info("Cleaning up...", "vi", vi.Name) + case pod == nil: + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_Provisioning + condition.Message = "DVCR Provisioner not found: create the new one." + + var envSettings *importer.Settings + envSettings, err = ds.getEnvSettings(vi, supgen) + if err != nil { + return false, err + } + + err = ds.importerService.Start(ctx, envSettings, vi, supgen, datasource.NewCABundleForVMI(vi.Spec.DataSource)) + if err != nil { + return false, err + } + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + ds.logger.Info("Ready", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", "nil") + case cc.IsPodComplete(pod): + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.ReadyReason_Ready + condition.Message = "" + + vi.Status.Phase = virtv2.ImageReady + vi.Status.Size = ds.statService.GetSize(pod) + vi.Status.CDROM = ds.statService.GetCDROM(pod) + vi.Status.Format = ds.statService.GetFormat(pod) + vi.Status.Progress = "100%" + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + ds.logger.Info("Ready", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + default: + err = ds.statService.CheckPod(pod) + if err != nil { + vi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_ProvisioningNotStarted + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + case errors.Is(err, service.ErrProvisioningFailed): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_ProvisioningFailed + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + default: + return false, err + } + } + + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_Provisioning + condition.Message = "Import is in the process of provisioning to DVCR." + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + ds.logger.Info("Ready", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + } + + return true, nil +} + +func (ds ObjectRefDataSource) CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + supgen := supplements.NewGenerator(util.ControllerShortName, vi.Name, vi.Namespace, vi.UID) + + requeue, err := ds.importerService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + + return requeue, nil +} + +func (ds ObjectRefDataSource) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { + if vi.Spec.DataSource.ObjectRef == nil { + return fmt.Errorf("nil object ref: %s", vi.Spec.DataSource.Type) + } + + dvcrDataSource, err := controller.NewDVCRDataSourcesForVMI(ctx, vi.Spec.DataSource, vi, ds.client) + if err != nil { + return err + } + + if dvcrDataSource.IsReady() { + return nil + } + + switch vi.Spec.DataSource.ObjectRef.Kind { + case virtv2.VirtualImageObjectRefKindVirtualImage: + return NewImageNotReadyError(vi.Spec.DataSource.ObjectRef.Name) + case virtv2.VirtualImageObjectRefKindClusterVirtualImage: + return NewClusterImageNotReadyError(vi.Spec.DataSource.ObjectRef.Name) + default: + return fmt.Errorf("unexpected object ref kind: %s", vi.Spec.DataSource.ObjectRef.Kind) + } +} + +func (ds ObjectRefDataSource) getEnvSettings(vi *virtv2.VirtualImage, supgen *supplements.Generator) (*importer.Settings, error) { + var settings importer.Settings + + switch vi.Spec.DataSource.ObjectRef.Kind { + case virtv2.VirtualImageObjectRefKindVirtualImage: + dvcrSourceImageName := ds.dvcrSettings.RegistryImageForVMI( + vi.Spec.DataSource.ObjectRef.Name, + vi.Namespace, + ) + importer.ApplyDVCRSourceSettings(&settings, dvcrSourceImageName) + case virtv2.VirtualImageObjectRefKindClusterVirtualImage: + dvcrSourceImageName := ds.dvcrSettings.RegistryImageForCVMI(vi.Spec.DataSource.ObjectRef.Name) + importer.ApplyDVCRSourceSettings(&settings, dvcrSourceImageName) + default: + return nil, fmt.Errorf("unknown objectRef kind: %s", vi.Spec.DataSource.ObjectRef.Kind) + } + + importer.ApplyDVCRDestinationSettings( + &settings, + ds.dvcrSettings, + supgen, + ds.dvcrSettings.RegistryImageForCVMI(vi.Name), + ) + + return &settings, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go new file mode 100644 index 000000000..75f7abdfb --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/registry.go @@ -0,0 +1,203 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "errors" + "fmt" + "log/slog" + + 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/client" + + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/controller/common" + "github.com/deckhouse/virtualization-controller/pkg/controller/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/util" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type RegistryDataSource struct { + statService Stat + importerService Importer + dvcrSettings *dvcr.Settings + client client.Client + logger *slog.Logger +} + +func NewRegistryDataSource( + statService Stat, + importerService Importer, + dvcrSettings *dvcr.Settings, + client client.Client, +) *RegistryDataSource { + return &RegistryDataSource{ + statService: statService, + importerService: importerService, + dvcrSettings: dvcrSettings, + client: client, + logger: slog.Default().With("controller", util.ControllerShortName, "ds", "registry"), + } +} + +func (ds RegistryDataSource) Sync(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + ds.logger.Info("Sync", "vi", vi.Name) + + condition, _ := service.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + defer func() { service.SetCondition(condition, &vi.Status.Conditions) }() + + supgen := supplements.NewGenerator(util.ControllerShortName, vi.Name, vi.Namespace, vi.UID) + pod, err := ds.importerService.GetPod(ctx, supgen) + if err != nil { + return false, err + } + + switch { + case isDiskProvisioningFinished(condition): + ds.logger.Info("Finishing...", "vi", vi.Name) + + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.ReadyReason_Ready + condition.Message = "" + + vi.Status.Phase = virtv2.ImageReady + + err = ds.importerService.Unprotect(ctx, pod) + if err != nil { + return false, err + } + + return CleanUp(ctx, vi, ds) + case common.IsTerminating(pod): + vi.Status.Phase = virtv2.ImagePending + + ds.logger.Info("Cleaning up...", "vi", vi.Name) + case pod == nil: + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_Provisioning + condition.Message = "DVCR Provisioner not found: create the new one." + + envSettings := ds.getEnvSettings(vi, supgen) + err = ds.importerService.Start(ctx, envSettings, vi, supgen, datasource.NewCABundleForVMI(vi.Spec.DataSource)) + if err != nil { + return false, err + } + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Progress = "0%" + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForVMI(vi.Name, vi.Namespace) + + ds.logger.Info("Create importer pod...", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", "nil") + case common.IsPodComplete(pod): + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.ReadyReason_Ready + condition.Message = "" + + vi.Status.Phase = virtv2.ImageReady + vi.Status.Size = ds.statService.GetSize(pod) + vi.Status.CDROM = ds.statService.GetCDROM(pod) + vi.Status.Format = ds.statService.GetFormat(pod) + vi.Status.Progress = "100%" + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForVMI(vi.Name, vi.Namespace) + + ds.logger.Info("Ready", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + default: + err = ds.statService.CheckPod(pod) + if err != nil { + vi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_ProvisioningNotStarted + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + case errors.Is(err, service.ErrProvisioningFailed): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_ProvisioningFailed + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + default: + return false, err + } + } + + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_Provisioning + condition.Message = "Import is in the process of provisioning to DVCR." + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Progress = "0%" + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForVMI(vi.Name, vi.Namespace) + + ds.logger.Info("Provisioning...", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + } + + return true, nil +} + +func (ds RegistryDataSource) CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + supgen := supplements.NewGenerator(util.ControllerShortName, vi.Name, vi.Namespace, vi.UID) + + requeue, err := ds.importerService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + + return requeue, nil +} + +func (ds RegistryDataSource) Validate(ctx context.Context, vi *virtv2.VirtualImage) error { + if vi.Spec.DataSource.ContainerImage.ImagePullSecret.Name != "" { + secretName := types.NamespacedName{ + Namespace: vi.Spec.DataSource.ContainerImage.ImagePullSecret.Namespace, + Name: vi.Spec.DataSource.ContainerImage.ImagePullSecret.Name, + } + secret, err := helper.FetchObject[*corev1.Secret](ctx, secretName, ds.client, &corev1.Secret{}) + if err != nil { + return fmt.Errorf("failed to get secret %s: %w", secretName, err) + } + + if secret == nil { + return ErrSecretNotFound + } + } + + return nil +} + +func (ds RegistryDataSource) getEnvSettings(vi *virtv2.VirtualImage, supgen *supplements.Generator) *importer.Settings { + var settings importer.Settings + + importer.ApplyRegistrySourceSettings(&settings, vi.Spec.DataSource.ContainerImage, supgen) + importer.ApplyDVCRDestinationSettings( + &settings, + ds.dvcrSettings, + supgen, + ds.dvcrSettings.RegistryImageForVMI(vi.Name, vi.Namespace), + ) + + return &settings +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go new file mode 100644 index 000000000..582ed4253 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go @@ -0,0 +1,87 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + cc "github.com/deckhouse/virtualization-controller/pkg/controller/common" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type Handler interface { + Sync(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) + CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) + Validate(ctx context.Context, vi *virtv2.VirtualImage) error +} + +type Sources struct { + sources map[virtv2.DataSourceType]Handler +} + +func NewSources() *Sources { + return &Sources{ + sources: make(map[virtv2.DataSourceType]Handler), + } +} + +func (s Sources) Set(dsType virtv2.DataSourceType, h Handler) { + s.sources[dsType] = h +} + +func (s Sources) For(dsType virtv2.DataSourceType) (Handler, bool) { + source, ok := s.sources[dsType] + return source, ok +} + +func (s Sources) Changed(_ context.Context, vi *virtv2.VirtualImage) bool { + return vi.Generation != vi.Status.ObservedGeneration +} + +func (s Sources) CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + var requeue bool + + for _, source := range s.sources { + ok, err := source.CleanUp(ctx, vi) + if err != nil { + return false, err + } + + requeue = requeue || ok + } + + return requeue, nil +} + +type Cleaner interface { + CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) +} + +func CleanUp(ctx context.Context, vi *virtv2.VirtualImage, c Cleaner) (bool, error) { + if cc.ShouldCleanupSubResources(vi) { + return c.CleanUp(ctx, vi) + } + + return false, nil +} + +func isDiskProvisioningFinished(c metav1.Condition) bool { + return c.Reason == vicondition.ReadyReason_Ready +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go new file mode 100644 index 000000000..652277e71 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.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 source + +import ( + "context" + "errors" + "fmt" + "log/slog" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization-controller/pkg/common/datasource" + "github.com/deckhouse/virtualization-controller/pkg/controller/common" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/util" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type UploadDataSource struct { + statService Stat + uploaderService Uploader + dvcrSettings *dvcr.Settings + logger *slog.Logger +} + +func NewUploadDataSource( + statService Stat, + uploaderService Uploader, + dvcrSettings *dvcr.Settings, +) *UploadDataSource { + return &UploadDataSource{ + statService: statService, + uploaderService: uploaderService, + dvcrSettings: dvcrSettings, + logger: slog.Default().With("controller", util.ControllerShortName, "ds", "upload"), + } +} + +func (ds UploadDataSource) Sync(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + ds.logger.Info("Sync", "vi", vi.Name) + + condition, _ := service.GetCondition(vicondition.ReadyType, vi.Status.Conditions) + defer func() { service.SetCondition(condition, &vi.Status.Conditions) }() + + supgen := supplements.NewGenerator(util.ControllerShortName, vi.Name, vi.Namespace, vi.UID) + pod, err := ds.uploaderService.GetPod(ctx, supgen) + if err != nil { + return false, err + } + svc, err := ds.uploaderService.GetService(ctx, supgen) + if err != nil { + return false, err + } + ing, err := ds.uploaderService.GetIngress(ctx, supgen) + if err != nil { + return false, err + } + + if vi.Status.UploadCommand == "" { + if ing != nil && ing.Annotations[common.AnnUploadURL] != "" { + vi.Status.UploadCommand = fmt.Sprintf("curl %s -T example.iso", ing.Annotations[common.AnnUploadURL]) + } + } + + switch { + case isDiskProvisioningFinished(condition): + ds.logger.Info("Finishing...", "vi", vi.Name) + + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.ReadyReason_Ready + condition.Message = "" + + vi.Status.Phase = virtv2.ImageReady + + err = ds.uploaderService.Unprotect(ctx, pod, svc, ing) + if err != nil { + return false, err + } + + return CleanUp(ctx, vi, ds) + case common.AnyTerminating(pod, svc, ing): + vi.Status.Phase = virtv2.ImagePending + + ds.logger.Info("Cleaning up...", "vi", vi.Name) + case pod == nil && svc == nil && ing == nil: + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_Provisioning + condition.Message = "DVCR Provisioner not found: create the new one." + + envSettings := ds.getEnvSettings(supgen) + err = ds.uploaderService.Start(ctx, envSettings, vi, supgen, datasource.NewCABundleForVMI(vi.Spec.DataSource)) + if err != nil { + return false, err + } + + vi.Status.Phase = virtv2.ImagePending + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + ds.logger.Info("Create uploader pod...", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", nil) + case common.IsPodComplete(pod): + condition.Status = metav1.ConditionTrue + condition.Reason = vicondition.ReadyReason_Ready + condition.Message = "" + + vi.Status.Phase = virtv2.ImageReady + vi.Status.Size = ds.statService.GetSize(pod) + vi.Status.CDROM = ds.statService.GetCDROM(pod) + vi.Status.Format = ds.statService.GetFormat(pod) + vi.Status.Progress = "100%" + vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + ds.logger.Info("Ready", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + case ds.statService.IsUploadStarted(vi.GetUID(), pod): + err = ds.statService.CheckPod(pod) + if err != nil { + vi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_ProvisioningNotStarted + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + case errors.Is(err, service.ErrProvisioningFailed): + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_ProvisioningFailed + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + default: + return false, err + } + } + + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_Provisioning + condition.Message = "Import is in the process of provisioning to DVCR." + + vi.Status.Phase = virtv2.ImageProvisioning + vi.Status.Progress = ds.statService.GetProgress(vi.GetUID(), pod, vi.Status.Progress) + vi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(vi.GetUID(), pod) + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + err = ds.uploaderService.Protect(ctx, pod, svc, ing) + if err != nil { + return false, err + } + + ds.logger.Info("Provisioning...", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + default: + condition.Status = metav1.ConditionFalse + condition.Reason = vicondition.ReadyReason_WaitForUserUpload + condition.Message = "Waiting for the user upload." + + vi.Status.Phase = virtv2.ImageWaitForUserUpload + vi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(vi.Name) + + ds.logger.Info("WaitForUserUpload...", "vi", vi.Name, "progress", vi.Status.Progress, "pod.phase", pod.Status.Phase) + } + + return true, nil +} + +func (ds UploadDataSource) CleanUp(ctx context.Context, vi *virtv2.VirtualImage) (bool, error) { + supgen := supplements.NewGenerator(util.ControllerShortName, vi.Name, vi.Namespace, vi.UID) + + requeue, err := ds.uploaderService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + + return requeue, nil +} + +func (ds UploadDataSource) Validate(_ context.Context, _ *virtv2.VirtualImage) error { + return nil +} + +func (ds UploadDataSource) getEnvSettings(supgen *supplements.Generator) *uploader.Settings { + var settings uploader.Settings + + uploader.ApplyDVCRDestinationSettings( + &settings, + ds.dvcrSettings, + supgen, + ds.dvcrSettings.RegistryImageForCVMI(supgen.Name), + ) + + return &settings +} diff --git a/images/virtualization-artifact/pkg/controller/cvmi_controller.go b/images/virtualization-artifact/pkg/controller/vi/internal/util/util.go similarity index 78% rename from images/virtualization-artifact/pkg/controller/cvmi_controller.go rename to images/virtualization-artifact/pkg/controller/vi/internal/util/util.go index 270e8ac72..fa3256232 100644 --- a/images/virtualization-artifact/pkg/controller/cvmi_controller.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/util/util.go @@ -14,13 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controller +package util -import ( - corev1 "k8s.io/api/core/v1" -) - -const ( - ImporterPodVerbose = "3" - ImporterPodPullPolicy = string(corev1.PullIfNotPresent) -) +const ControllerShortName = "vi" diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go new file mode 100644 index 000000000..2378ce786 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/vi_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 vi + +import ( + "context" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/source" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +const ( + ControllerName = "vi-controller" + + PodVerbose = "3" + PodPullPolicy = string(corev1.PullIfNotPresent) +) + +type Condition interface { + Handle(ctx context.Context, vi *virtv2.VirtualImage) error +} + +func NewController( + ctx context.Context, + mgr manager.Manager, + log logr.Logger, + importerImage string, + uploaderImage string, + dvcr *dvcr.Settings, +) (controller.Controller, error) { + stat := service.NewStatService() + protection := service.NewProtectionService(mgr.GetClient(), virtv2.FinalizerVirtualImageProtection) + importer := service.NewImporterService(dvcr, mgr.GetClient(), importerImage, PodPullPolicy, PodVerbose, ControllerName, protection) + uploader := service.NewUploaderService(dvcr, mgr.GetClient(), uploaderImage, PodPullPolicy, PodVerbose, ControllerName, protection) + + sources := source.NewSources() + sources.Set(virtv2.DataSourceTypeHTTP, source.NewHTTPDataSource(stat, importer, dvcr)) + sources.Set(virtv2.DataSourceTypeContainerImage, source.NewRegistryDataSource(stat, importer, dvcr, mgr.GetClient())) + sources.Set(virtv2.DataSourceTypeObjectRef, source.NewObjectRefDataSource(stat, importer, dvcr, mgr.GetClient())) + sources.Set(virtv2.DataSourceTypeUpload, source.NewUploadDataSource(stat, uploader, dvcr)) + + reconciler := NewReconciler( + mgr.GetClient(), + internal.NewDatasourceReadyHandler(sources), + internal.NewLifeCycleHandler(sources, mgr.GetClient()), + internal.NewDeletionHandler(sources), + ) + + viController, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: reconciler}) + if err != nil { + return nil, err + } + + err = reconciler.SetupController(ctx, mgr, viController) + if err != nil { + return nil, err + } + + if err = builder.WebhookManagedBy(mgr). + For(&virtv2.VirtualImage{}). + WithValidator(NewValidator()). + Complete(); err != nil { + return nil, err + } + + log.Info("Initialized VirtualImage controller", "image", importerImage) + + return viController, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go new file mode 100644 index 000000000..ecb3ecabb --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go @@ -0,0 +1,132 @@ +/* +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 vi + +import ( + "context" + "errors" + "log/slog" + "time" + + "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" +) + +type Handler interface { + Handle(ctx context.Context, vi *virtv2.VirtualImage) (reconcile.Result, error) + Name() string +} + +type Reconciler struct { + handlers []Handler + client client.Client +} + +func NewReconciler(client client.Client, handlers ...Handler) *Reconciler { + return &Reconciler{ + client: client, + handlers: handlers, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + vi := service.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) + + err := vi.Fetch(ctx) + if err != nil { + return reconcile.Result{}, err + } + + if vi.IsEmpty() { + return reconcile.Result{}, nil + } + + var requeue bool + + slog.Info("Start reconcile VI", slog.String("namespacedName", req.String())) + + var handlerErrs []error + + for _, h := range r.handlers { + slog.Info("Run handler", slog.String("name", h.Name())) + var res reconcile.Result + res, err = h.Handle(ctx, vi.Changed()) + if err != nil { + slog.Error("Failed to handle vi", "err", err) + handlerErrs = append(handlerErrs, err) + } + + requeue = requeue || res.Requeue + } + + vi.Changed().Status.ObservedGeneration = vi.Changed().Generation + + err = vi.Update(ctx) + if err != nil { + return reconcile.Result{}, err + } + + err = errors.Join(handlerErrs...) + if err != nil { + return reconcile.Result{}, err + } + + if requeue { + slog.Info("Requeue for VI", slog.String("namespacedName", req.String())) + return reconcile.Result{ + RequeueAfter: 5 * time.Second, + }, nil + } + + slog.Info("Finished reconcile VI", slog.String("namespacedName", req.String())) + return reconcile.Result{}, nil +} + +func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { + if err := ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualImage{}), + &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() + }, + }, + ); err != nil { + return err + } + + return nil +} + +func (r *Reconciler) factory() *virtv2.VirtualImage { + return &virtv2.VirtualImage{} +} + +func (r *Reconciler) statusGetter(obj *virtv2.VirtualImage) virtv2.VirtualImageStatus { + return obj.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go new file mode 100644 index 000000000..d27a48f5b --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go @@ -0,0 +1,79 @@ +/* +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 vi + +import ( + "context" + "fmt" + "log/slog" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/util" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type Validator struct { + logger *slog.Logger +} + +func NewValidator() *Validator { + return &Validator{ + logger: slog.Default().With("controller", util.ControllerShortName, "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) { + oldVI, ok := oldObj.(*virtv2.VirtualImage) + if !ok { + return nil, fmt.Errorf("expected an old VirtualImage but got a %T", newObj) + } + + newVI, ok := newObj.(*virtv2.VirtualImage) + if !ok { + return nil, fmt.Errorf("expected a new VirtualImage but got a %T", newObj) + } + + v.logger.Info("Validating VirtualImage") + + if oldVI.Generation == newVI.Generation { + return nil, nil + } + + ready, _ := service.GetCondition(vicondition.ReadyType, newVI.Status.Conditions) + if newVI.Status.Phase == virtv2.ImageReady || ready.Status == metav1.ConditionTrue { + return nil, fmt.Errorf("VirtualImage is in the Ready state: spec is immutable now") + } + + 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/vmi_controller.go b/images/virtualization-artifact/pkg/controller/vmi_controller.go deleted file mode 100644 index 086ee6c40..000000000 --- a/images/virtualization-artifact/pkg/controller/vmi_controller.go +++ /dev/null @@ -1,76 +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" - "time" - - "github.com/go-logr/logr" - "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/manager" - - "github.com/deckhouse/virtualization-controller/pkg/dvcr" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" -) - -const ( - vmiControllerName = "vi-controller" - vmiShortName = "vi" -) - -func NewVMIController( - ctx context.Context, - mgr manager.Manager, - log logr.Logger, - importerImage string, - uploaderImage string, - dvcrSettings *dvcr.Settings, -) (controller.Controller, error) { - reconciler := NewVMIReconciler( - importerImage, - uploaderImage, - ImporterPodVerbose, - ImporterPodPullPolicy, - dvcrSettings, - ) - - reconcilerCore := two_phase_reconciler.NewReconcilerCore[*VMIReconcilerState]( - reconciler, - NewVMIReconcilerState, - two_phase_reconciler.ReconcilerOptions{ - Client: mgr.GetClient(), - Cache: mgr.GetCache(), - Recorder: mgr.GetEventRecorderFor(vmiControllerName), - Scheme: mgr.GetScheme(), - Log: log.WithName(vmiControllerName), - }) - - c, err := controller.New(vmiControllerName, mgr, controller.Options{ - Reconciler: reconcilerCore, - RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Second, 32*time.Second), - }) - if err != nil { - return nil, err - } - if err := reconciler.SetupController(ctx, mgr, c); err != nil { - return nil, err - } - log.Info("Initialized VirtualImage controller") - return c, nil -} diff --git a/images/virtualization-artifact/pkg/controller/vmi_datavolume.go b/images/virtualization-artifact/pkg/controller/vmi_datavolume.go deleted file mode 100644 index f7dcad86e..000000000 --- a/images/virtualization-artifact/pkg/controller/vmi_datavolume.go +++ /dev/null @@ -1,147 +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" - - "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - - dvutil "github.com/deckhouse/virtualization-controller/pkg/common/datavolume" - vmiutil "github.com/deckhouse/virtualization-controller/pkg/common/vmi" - "github.com/deckhouse/virtualization-controller/pkg/controller/kvbuilder" - "github.com/deckhouse/virtualization-controller/pkg/controller/monitoring" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -// getPVCSize retrieves PVC size from importer Pod final report after import is done. -func (r *VMIReconciler) getPVCSize(vmi *virtv2.VirtualImage, state *VMIReconcilerState) (resource.Quantity, error) { - var unpackedSize resource.Quantity - - switch { - case vmiutil.IsTwoPhaseImport(vmi): - // Get size from the importer Pod to detect if specified PVC size is enough. - finalReport, err := monitoring.GetFinalReportFromPod(state.Pod) - if err != nil { - return resource.Quantity{}, fmt.Errorf("cannot create PVC without final report from the Pod: %w", err) - } - - unpackedSize = *resource.NewQuantity(int64(finalReport.UnpackedSizeBytes), resource.BinarySI) - case vmiutil.IsDVCRSource(vmi): - var err error - unpackedSize, err = resource.ParseQuantity(state.DVCRDataSource.GetSize().UnpackedBytes) - if err != nil { - return resource.Quantity{}, err - } - default: - return resource.Quantity{}, errors.New("failed to get unpacked size from data source") - } - - if unpackedSize.IsZero() { - return resource.Quantity{}, errors.New("got zero unpacked size from data source") - } - - // Adjust PVC size to feat image onto scratch PVC. - // TODO(future): remove after get rid of scratch. - size := dvutil.AdjustPVCSize(unpackedSize) - - return size, nil -} - -func (r *VMIReconciler) createDataVolume(ctx context.Context, vmi *virtv2.VirtualImage, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - // Retrieve PVC size. - pvcSize, err := r.getPVCSize(vmi, state) - if err != nil { - return err - } - - dv, err := r.makeDataVolumeFromVMI(state, state.Supplements.DataVolume(), pvcSize) - if err != nil { - return err - } - - if err = opts.Client.Create(ctx, dv); err != nil { - opts.Log.V(2).Info("Error create new DV spec", "dv.spec", dv.Spec) - return fmt.Errorf("create DataVolume/%s for VI/%s: %w", dv.GetName(), vmi.GetName(), err) - } - opts.Log.Info("Created new DV", "dv.name", dv.GetName()) - opts.Log.V(2).Info("Created new DV spec", "dv.spec", dv.Spec) - - if vmiutil.IsTwoPhaseImport(vmi) || vmiutil.IsDVCRSource(vmi) { - // Copy auth credentials and ca bundle to access DVCR as 'registry' data source. - // Set DV as an ownerRef to auto-cleanup these copies. - err = supplements.EnsureForDataVolume(ctx, opts.Client, state.Supplements, dv, r.dvcrSettings) - if err != nil { - return fmt.Errorf("failed to ensure data volume supplements: %w", err) - } - } - - return nil -} - -// makeDataVolumeFromVMD makes DataVolume with 'registry' dataSource to import -// DVCR image onto PVC. -func (r *VMIReconciler) makeDataVolumeFromVMI(state *VMIReconcilerState, dvName types.NamespacedName, pvcSize resource.Quantity) (*cdiv1.DataVolume, error) { - dvBuilder := kvbuilder.NewDV(dvName) - vmi := state.VMI.Current() - ds := vmi.Spec.DataSource - - authSecretName := state.Supplements.DVCRAuthSecretForDV().Name - caBundleName := state.Supplements.DVCRCABundleConfigMapForDV().Name - - // Set datasource: - // 'registry' if import is two phased. - switch { - case vmiutil.IsTwoPhaseImport(vmi): - // The image was preloaded from source into dvcr. - // We can't use the same data source a second time, but we can set dvcr as the data source. - // Use DV name for the Secret with DVCR auth and the ConfigMap with DVCR CA Bundle. - dvcrSourceImageName := r.dvcrSettings.RegistryImageForVMI(vmi.Name, vmi.Namespace) - dvBuilder.SetRegistryDataSource(dvcrSourceImageName, authSecretName, caBundleName) - case ds.Type == virtv2.DataSourceTypeObjectRef: - if ds.ObjectRef == nil { - return nil, fmt.Errorf("nil objectRef %q", vmiutil.GetDataSourceType(vmi)) - } - - switch ds.ObjectRef.Kind { - case virtv2.VirtualImageObjectRefKindVirtualImage: - // NOTE: use namespace from current VMI. - dvcrSourceImageName := r.dvcrSettings.RegistryImageForVMI(ds.ObjectRef.Name, vmi.Namespace) - dvBuilder.SetRegistryDataSource(dvcrSourceImageName, authSecretName, caBundleName) - case virtv2.VirtualImageObjectRefKindClusterVirtualImage: - dvcrSourceImageName := r.dvcrSettings.RegistryImageForCVMI(ds.ObjectRef.Name) - dvBuilder.SetRegistryDataSource(dvcrSourceImageName, authSecretName, caBundleName) - default: - return nil, fmt.Errorf("unsupported object ref kind %q", ds.ObjectRef.Kind) - } - default: - return nil, fmt.Errorf("unsupported dataSource type %q", vmiutil.GetDataSourceType(vmi)) - } - - dvBuilder.SetPVC(vmi.Spec.PersistentVolumeClaim.StorageClass, pvcSize) - - dvBuilder.SetOwnerRef(vmi, vmi.GetObjectKind().GroupVersionKind()) - dvBuilder.AddFinalizer(virtv2.FinalizerDVProtection) - - return dvBuilder.GetResource(), nil -} diff --git a/images/virtualization-artifact/pkg/controller/vmi_importer.go b/images/virtualization-artifact/pkg/controller/vmi_importer.go deleted file mode 100644 index e75f30b1c..000000000 --- a/images/virtualization-artifact/pkg/controller/vmi_importer.go +++ /dev/null @@ -1,119 +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/deckhouse/virtualization-controller/pkg/common/datasource" - vmiutil "github.com/deckhouse/virtualization-controller/pkg/common/vmi" - cc "github.com/deckhouse/virtualization-controller/pkg/controller/common" - "github.com/deckhouse/virtualization-controller/pkg/controller/importer" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" - virtv2alpha1 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -func (r *VMIReconciler) startImporterPod(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - vmi := state.VMI.Current() - opts.Log.V(1).Info("Creating importer POD for VI", "vi.Name", vmi.Name) - - importerSettings, err := r.createImporterSettings(state) - if err != nil { - return err - } - - // all checks passed, let's create the importer pod! - podSettings := r.createImporterPodSettings(state) - - imp := importer.NewImporter(podSettings, importerSettings) - pod, err := imp.CreatePod(ctx, opts.Client) - if err != nil { - err = cc.PublishPodErr(err, podSettings.Name, vmi, opts.Recorder, opts.Client) - if err != nil { - return err - } - } - - opts.Log.V(1).Info("Created importer POD", "pod.Name", pod.Name) - - // Ensure supplement resources for the Pod. - return supplements.EnsureForPod(ctx, opts.Client, state.Supplements, pod, datasource.NewCABundleForVMI(vmi.Spec.DataSource), r.dvcrSettings) -} - -// createImporterSettings fills settings for the dvcr-importer binary. -func (r *VMIReconciler) createImporterSettings(state *VMIReconcilerState) (*importer.Settings, error) { - settings := &importer.Settings{ - Verbose: r.verbose, - } - - vmi := state.VMI.Current() - ds := vmi.Spec.DataSource - - switch ds.Type { - case virtv2alpha1.DataSourceTypeHTTP: - if ds.HTTP == nil { - return nil, fmt.Errorf("dataSource '%s' specified without related 'http' section", ds.Type) - } - - importer.ApplyHTTPSourceSettings(settings, ds.HTTP, state.Supplements) - case virtv2alpha1.DataSourceTypeContainerImage: - if ds.ContainerImage == nil { - return nil, fmt.Errorf("dataSource '%s' specified without related 'containerImage' section", ds.Type) - } - importer.ApplyRegistrySourceSettings(settings, ds.ContainerImage, state.Supplements) - case virtv2alpha1.DataSourceTypeObjectRef: - if ds.ObjectRef == nil { - return nil, fmt.Errorf("dataSource '%s' specified without related 'objectRef' section", ds.Type) - } - - switch ds.ObjectRef.Kind { - case virtv2alpha1.VirtualImageObjectRefKindVirtualImage: - dvcrSourceImageName := r.dvcrSettings.RegistryImageForVMI(ds.ObjectRef.Name, vmi.Namespace) - importer.ApplyDVCRSourceSettings(settings, dvcrSourceImageName) - case virtv2alpha1.VirtualImageObjectRefKindClusterVirtualImage: - dvcrSourceImageName := r.dvcrSettings.RegistryImageForCVMI(ds.ObjectRef.Name) - importer.ApplyDVCRSourceSettings(settings, dvcrSourceImageName) - default: - return nil, fmt.Errorf("unknown objectRef kind: %s", ds.ObjectRef.Kind) - } - default: - return nil, fmt.Errorf("unknown dataSource: %s", ds.Type) - } - - // Set DVCR destination settings. - dvcrDestImageName := r.dvcrSettings.RegistryImageForVMI(vmi.Name, vmi.Namespace) - importer.ApplyDVCRDestinationSettings(settings, r.dvcrSettings, state.Supplements, dvcrDestImageName) - - // TODO Update proxy settings. - - return settings, nil -} - -func (r *VMIReconciler) createImporterPodSettings(state *VMIReconcilerState) *importer.PodSettings { - importerPod := state.Supplements.ImporterPod() - return &importer.PodSettings{ - Name: importerPod.Name, - Image: r.importerImage, - PullPolicy: r.pullPolicy, - Namespace: importerPod.Namespace, - OwnerReference: vmiutil.MakeOwnerReference(state.VMI.Current()), - ControllerName: vmiControllerName, - InstallerLabels: map[string]string{}, - } -} diff --git a/images/virtualization-artifact/pkg/controller/vmi_reconciler.go b/images/virtualization-artifact/pkg/controller/vmi_reconciler.go deleted file mode 100644 index ca4799b02..000000000 --- a/images/virtualization-artifact/pkg/controller/vmi_reconciler.go +++ /dev/null @@ -1,706 +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" - "strconv" - "time" - - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" - "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/common" - vmiutil "github.com/deckhouse/virtualization-controller/pkg/common/vmi" - cc "github.com/deckhouse/virtualization-controller/pkg/controller/common" - "github.com/deckhouse/virtualization-controller/pkg/controller/importer" - "github.com/deckhouse/virtualization-controller/pkg/controller/monitoring" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" - "github.com/deckhouse/virtualization-controller/pkg/controller/vmattachee" - "github.com/deckhouse/virtualization-controller/pkg/dvcr" - "github.com/deckhouse/virtualization-controller/pkg/imageformat" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" - "github.com/deckhouse/virtualization-controller/pkg/util" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type VMIReconciler struct { - *vmattachee.AttacheeReconciler[*virtv2.VirtualImage, virtv2.VirtualImageStatus] - - importerImage string - uploaderImage string - verbose string - pullPolicy string - dvcrSettings *dvcr.Settings -} - -func NewVMIReconciler(importerImage, uploaderImage, verbose, pullPolicy string, dvcrSettings *dvcr.Settings) *VMIReconciler { - return &VMIReconciler{ - importerImage: importerImage, - uploaderImage: uploaderImage, - verbose: verbose, - pullPolicy: pullPolicy, - dvcrSettings: dvcrSettings, - AttacheeReconciler: vmattachee.NewAttacheeReconciler[ - *virtv2.VirtualImage, - virtv2.VirtualImageStatus, - ](), - } -} - -func (r *VMIReconciler) SetupController(ctx context.Context, mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch(source.Kind(mgr.GetCache(), &virtv2.VirtualImage{}), &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 }, - }, - ); err != nil { - return fmt.Errorf("error setting watch on VI: %w", err) - } - - if err := ctr.Watch( - source.Kind(mgr.GetCache(), &cdiv1.DataVolume{}), - handler.EnqueueRequestForOwner( - mgr.GetScheme(), - mgr.GetRESTMapper(), - &virtv2.VirtualImage{}, - handler.OnlyControllerOwner(), - ), - ); err != nil { - return fmt.Errorf("error setting watch on DV: %w", err) - } - - if err := ctr.Watch( - source.Kind(mgr.GetCache(), &corev1.PersistentVolumeClaim{}), - handler.EnqueueRequestForOwner( - mgr.GetScheme(), - mgr.GetRESTMapper(), - &virtv2.VirtualImage{}, - handler.OnlyControllerOwner(), - ), predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { return false }, - DeleteFunc: func(e event.DeleteEvent) bool { return true }, - UpdateFunc: func(e event.UpdateEvent) bool { return false }, - }, - ); err != nil { - return fmt.Errorf("error setting watch on PVC: %w", err) - } - - return r.AttacheeReconciler.SetupController(mgr, ctr, r) -} - -// Sync starts an importer/uploader Pod or creates a DataVolume to import image into DVCR or into PVC. -// There are 3 modes of import: -// - Start and track importer/uploader Pod only (e.g. dataSource is HTTP and storage is ContainerRegistry). -// - Start importer/uploader Pod first and then create DataVolume (e.g. target size is unknown: dataSource is HTTP and storage is Kubernetes without specified size for PVC). -// - Create and track DataVolume only (e.g. dataSource is ClusterVirtualImage and storage is Kubernetes). -func (r *VMIReconciler) Sync(ctx context.Context, _ reconcile.Request, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - if r.AttacheeReconciler.Sync(ctx, state.AttacheeState, opts) { - return nil - } - - switch { - case state.IsDeletion(): - opts.Log.V(1).Info("Delete VI, remove protective finalizers") - return r.cleanupOnDeletion(ctx, state, opts) - case !state.IsProtected(): - // Set protective finalizer atomically. - if controllerutil.AddFinalizer(state.VMI.Changed(), virtv2.FinalizerVMICleanup) { - state.SetReconcilerResult(&reconcile.Result{Requeue: true}) - return nil - } - case state.IsReady(): - opts.Log.Info("VI ready: cleanup underlying resources") - // Delete underlying importer/uploader Pod, Service and DataVolume and stop the reconcile process. - if cc.ShouldCleanupSubResources(state.VMI.Current()) { - return r.cleanup(ctx, state.VMI.Changed(), state.Client, state, opts) - } - return nil - case state.IsLost(): - return nil - case state.IsFailed(): - opts.Log.Info("VI failed: cleanup underlying resources") - // Delete underlying importer/uploader Pod, Service and DataVolume and stop the reconcile process. - if cc.ShouldCleanupSubResources(state.VMI.Current()) { - err := r.cleanup(ctx, state.VMI.Changed(), state.Client, state, opts) - if err != nil { - return err - } - } - - return nil - case state.ShouldTrackPod() && !state.IsPodComplete(): - // Start and track importer/uploader Pod. - switch { - case vmiutil.IsDVCRSource(state.VMI.Current()) && !state.DVCRDataSource.IsReady(): - opts.Log.V(1).Info("Wait for the data source to be ready") - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - case state.CanStartPod(): - // Create Pod using name and namespace from annotation. - opts.Log.V(1).Info("Start new Pod for VI") - if err := r.verifyDataSource(state); err != nil { - return err - } - - // Create importer/uploader pod, make sure the VMI owns it. - if err := r.startPod(ctx, state, opts); err != nil { - return err - } - // Requeue to wait until Pod become Running. - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - case state.Pod != nil: - // Import is in progress, force a re-reconcile in 2 seconds to update status. - opts.Log.V(2).Info("Requeue: wait until Pod is completed", "vi.name", state.VMI.Current().Name) - if err := r.ensurePodFinalizers(ctx, state, opts); err != nil { - return err - } - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - } - case !state.ShouldTrackDataVolume() && state.ShouldTrackPod() && state.IsPodComplete(): - // Proceed to UpdateStatus and requeue to handle Ready state. - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: time.Second}) - return nil - case state.ShouldTrackDataVolume() && (!state.ShouldTrackPod() || state.IsPodComplete()): - // Start and track DataVolume. - switch { - case vmiutil.IsDVCRSource(state.VMI.Current()) && !state.DVCRDataSource.IsReady(): - opts.Log.V(1).Info("Wait for the data source to be ready") - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - case state.CanCreateDataVolume(): - opts.Log.V(1).Info("Create DataVolume for VI") - - if err := r.createDataVolume(ctx, state.VMI.Current(), state, opts); err != nil { - return err - } - // Requeue to wait until Pod become Running. - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - case state.DV != nil: - // Import is in progress, force a re-reconcile in 2 seconds to update status. - opts.Log.V(2).Info("Requeue: wait until DataVolume is completed", "vi.name", state.VMI.Current().Name) - - if state.IsDataVolumeComplete() { - err := r.setPVCOwnerReference(ctx, state, opts.Client) - if err != nil { - return err - } - } - - err := r.ensureDVFinalizers(ctx, state, opts) - if err != nil { - return err - } - - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - } - } - - // Report unexpected state. - details := fmt.Sprintf("vi.Status.Phase='%s'", state.VMI.Current().Status.Phase) - if state.Pod != nil { - details += fmt.Sprintf(" pod.Name='%s' pod.Status.Phase='%s'", state.Pod.Name, state.Pod.Status.Phase) - } - if state.DV != nil { - details += fmt.Sprintf(" dv.Name='%s' dv.Status.Phase='%s'", state.DV.Name, state.DV.Status.Phase) - } - if state.PVC != nil { - details += fmt.Sprintf(" pvc.Name='%s' pvc.Status.Phase='%s'", state.PVC.Name, state.PVC.Status.Phase) - } - opts.Recorder.Event(state.VMI.Current(), corev1.EventTypeWarning, virtv2.ReasonErrUnknownState, fmt.Sprintf("VI has unexpected state, recreate it to start import again. %s", details)) - - return nil -} - -func (r *VMIReconciler) UpdateStatus(_ context.Context, _ reconcile.Request, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.V(2).Info("Update VI status", "vi.name", state.VMI.Current().GetName()) - - // Do nothing if object is being deleted as any update will lead to en error. - if state.IsDeletion() { - return nil - } - - // Record event if importer/uploader Pod has error. - // TODO set Failed status if Pod restarts are greater than some threshold? - if state.Pod != nil && len(state.Pod.Status.ContainerStatuses) > 0 { - if state.Pod.Status.ContainerStatuses[0].LastTerminationState.Terminated != nil && - state.Pod.Status.ContainerStatuses[0].LastTerminationState.Terminated.ExitCode > 0 { - opts.Recorder.Event(state.VMI.Current(), corev1.EventTypeWarning, virtv2.ReasonErrImportFailed, fmt.Sprintf("pod phase '%s', message '%s'", state.Pod.Status.Phase, state.Pod.Status.ContainerStatuses[0].LastTerminationState.Terminated.Message)) - } - } - - vmiStatus := state.VMI.Current().Status.DeepCopy() - switch { - case vmiStatus.Phase == "": - vmiStatus.Phase = virtv2.ImagePending - state.SetReconcilerResult(&reconcile.Result{Requeue: true}) - if err := r.verifyDataSource(state); err != nil { - vmiStatus.FailureReason = FailureReasonCannotBeProcessed - vmiStatus.FailureMessage = fmt.Sprintf("DataSource is invalid. %s", err) - } - case state.IsFailed(), state.IsLost(): - // No need to update status. - break - case state.IsReady(): - if state.ShouldTrackDataVolume() && state.PVC == nil { - opts.Log.Info("PVC not found for ready vi with kubernetes storage") - vmiStatus.Phase = virtv2.ImagePVCLost - } - case state.ShouldTrackPod() && state.IsPodInProgress(): - opts.Log.V(2).Info("Fetch progress from Pod", "vi.name", state.VMI.Current().GetName()) - - vmiStatus.Phase = virtv2.ImageProvisioning - if state.VMI.Current().Spec.DataSource.Type == virtv2.DataSourceTypeUpload && - vmiStatus.UploadCommand == "" && - state.Ingress != nil && - state.Ingress.GetAnnotations()[cc.AnnUploadURL] != "" { - vmiStatus.UploadCommand = fmt.Sprintf( - "curl %s -T example.iso", - state.Ingress.GetAnnotations()[cc.AnnUploadURL], - ) - } - - var progress *monitoring.ImportProgress - if !vmiutil.IsDVCRSource(state.VMI.Current()) { - var err error - progress, err = monitoring.GetImportProgressFromPod(string(state.VMI.Current().GetUID()), state.Pod) - if err != nil { - opts.Recorder.Event(state.VMI.Current(), corev1.EventTypeWarning, virtv2.ReasonErrGetProgressFailed, "Error fetching progress metrics from Pod "+err.Error()) - return err - } - if progress != nil { - opts.Log.V(2).Info("Got progress", "vi.name", state.VMI.Current().Name, "progress", progress.Progress(), "speed", progress.AvgSpeed(), "progress.raw", progress.ProgressRaw(), "speed.raw", progress.AvgSpeedRaw()) - // map 0-100% to 0-50%. - progressPct := progress.Progress() - if state.ShouldTrackDataVolume() { - progressPct = common.ScalePercentage(progressPct, 0, 50.0) - } - vmiStatus.Progress = progressPct - vmiStatus.DownloadSpeed.Avg = progress.AvgSpeed() - vmiStatus.DownloadSpeed.AvgBytes = strconv.FormatUint(progress.AvgSpeedRaw(), 10) - vmiStatus.DownloadSpeed.Current = progress.CurSpeed() - vmiStatus.DownloadSpeed.CurrentBytes = strconv.FormatUint(progress.CurSpeedRaw(), 10) - } - } - - // Set VMI phase. - if state.VMI.Current().Spec.DataSource.Type == virtv2.DataSourceTypeUpload && (progress == nil || progress.ProgressRaw() == 0) { - vmiStatus.Phase = virtv2.ImageWaitForUserUpload - // Fail if uploading time has expired. - if helper.GetAge(state.Pod) > cc.UploaderWaitDuration { - vmiStatus.Phase = virtv2.ImageFailed - vmiStatus.FailureReason = virtv2.ReasonErrUploaderWaitDurationExpired - vmiStatus.FailureMessage = "uploading time expired" - } - } else { - vmiStatus.Phase = virtv2.ImageProvisioning - } - case !state.ShouldTrackDataVolume() && state.ShouldTrackPod() && state.IsPodComplete(): - vmiStatus.Phase = virtv2.ImageReady - vmiStatus.Progress = "100%" - - opts.Log.V(1).Info("Import completed successfully") - - opts.Recorder.Event(state.VMI.Current(), corev1.EventTypeNormal, virtv2.ReasonImportSucceeded, "Import Successful") - - // Cleanup - vmiStatus.DownloadSpeed.Current = "" - vmiStatus.DownloadSpeed.CurrentBytes = "" - - switch { - case vmiutil.IsDVCRSource(state.VMI.Current()): - vmiStatus.Format = state.DVCRDataSource.GetFormat() - vmiStatus.CDROM = imageformat.IsISO(vmiStatus.Format) - vmiStatus.Size = state.DVCRDataSource.GetSize() - default: - finalReport, err := monitoring.GetFinalReportFromPod(state.Pod) - if err != nil { - return err - } - - if finalReport.ErrMessage != "" { - vmiStatus.Phase = virtv2.ImageFailed - vmiStatus.FailureReason = virtv2.ReasonErrImportFailed - vmiStatus.FailureMessage = finalReport.ErrMessage - opts.Recorder.Event(state.VMI.Current(), corev1.EventTypeWarning, virtv2.ReasonErrImportFailed, finalReport.ErrMessage) - - break - } - - vmiStatus.Format = finalReport.Format - vmiStatus.CDROM = imageformat.IsISO(vmiStatus.Format) - vmiStatus.DownloadSpeed.Avg = finalReport.GetAverageSpeed() - vmiStatus.DownloadSpeed.AvgBytes = strconv.FormatUint(finalReport.GetAverageSpeedRaw(), 10) - vmiStatus.Size.Stored = finalReport.StoredSize() - vmiStatus.Size.StoredBytes = strconv.FormatUint(finalReport.StoredSizeBytes, 10) - vmiStatus.Size.Unpacked = finalReport.UnpackedSize() - vmiStatus.Size.UnpackedBytes = strconv.FormatUint(finalReport.UnpackedSizeBytes, 10) - } - - // Set target image name the same way as for the importer/uploader Pod. - dvcrDestImageName := r.dvcrSettings.RegistryImageForVMI(state.VMI.Current().Name, state.VMI.Current().Namespace) - vmiStatus.Target.RegistryURL = dvcrDestImageName - case state.ShouldTrackDataVolume() && state.CanCreateDataVolume(): - finalReport, err := monitoring.GetFinalReportFromPod(state.Pod) - if err != nil { - return err - } - - if finalReport.ErrMessage != "" { - vmiStatus.Phase = virtv2.ImageFailed - vmiStatus.FailureReason = virtv2.ReasonErrImportFailed - vmiStatus.FailureMessage = finalReport.ErrMessage - opts.Recorder.Event(state.VMI.Current(), corev1.EventTypeWarning, virtv2.ReasonErrImportFailed, finalReport.ErrMessage) - } - - vmiStatus.DownloadSpeed.Current = "" - vmiStatus.DownloadSpeed.CurrentBytes = "" - vmiStatus.DownloadSpeed.Avg = finalReport.GetAverageSpeed() - vmiStatus.DownloadSpeed.AvgBytes = strconv.FormatUint(finalReport.GetAverageSpeedRaw(), 10) - case state.ShouldTrackDataVolume() && state.IsDataVolumeInProgress(): - // Set phase from DataVolume resource. - vmiStatus.Phase = MapDataVolumePhaseToVMIPhase(state.DV.Status.Phase) - - // Download speed is not available from DataVolume. - vmiStatus.DownloadSpeed.Current = "" - vmiStatus.DownloadSpeed.CurrentBytes = "" - - // Copy progress from DataVolume. - // map 0-100% to 50%-100%. - dvProgress := string(state.DV.Status.Progress) - - opts.Log.V(2).Info("Got DataVolume progress", "progress", dvProgress) - - if dvProgress != "N/A" && dvProgress != "" { - vmiStatus.Progress = common.ScalePercentage(dvProgress, 50.0, 100.0) - } - - // Copy capacity from PVC. - if state.PVC != nil && state.PVC.Status.Phase == corev1.ClaimBound { - vmiStatus.Capacity = util.GetPointer(state.PVC.Status.Capacity[corev1.ResourceStorage]).String() - } - case state.ShouldTrackDataVolume() && state.IsDataVolumeComplete(): - if state.PVC == nil { - opts.Log.Info("PVC not found for completed vi") - vmiStatus.Phase = virtv2.ImagePVCLost - break - } - - if state.PVC.Status.Phase != corev1.ClaimBound { - opts.Log.Info("Wait for the PVC to enter the Bound phase") - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - break - } - - opts.Recorder.Event(state.VMI.Current(), corev1.EventTypeNormal, virtv2.ReasonImportSucceededToPVC, "Import Successful") - opts.Log.V(1).Info("Import completed successfully") - vmiStatus.Phase = virtv2.ImageReady - vmiStatus.Progress = "100%" - - // Cleanup. - vmiStatus.DownloadSpeed.Current = "" - vmiStatus.DownloadSpeed.CurrentBytes = "" - // PVC name equals to the DataVolume name. - dv := state.Supplements.DataVolume() - vmiStatus.Target.PersistentVolumeClaim = dv.Name - - // Copy capacity from PVC if IsDataVolumeInProgress was very quick. - vmiStatus.Capacity = util.GetPointer(state.PVC.Status.Capacity[corev1.ResourceStorage]).String() - } - - state.VMI.Changed().Status = *vmiStatus - - return nil -} - -func MapDataVolumePhaseToVMIPhase(phase cdiv1.DataVolumePhase) virtv2.ImagePhase { - switch phase { - case cdiv1.PhaseUnset, cdiv1.Unknown, cdiv1.Pending: - return virtv2.ImagePending - case cdiv1.WaitForFirstConsumer, cdiv1.PVCBound, - cdiv1.ImportScheduled, cdiv1.CloneScheduled, cdiv1.UploadScheduled, - cdiv1.ImportInProgress, cdiv1.CloneInProgress, - cdiv1.SnapshotForSmartCloneInProgress, cdiv1.SmartClonePVCInProgress, - cdiv1.CSICloneInProgress, - cdiv1.CloneFromSnapshotSourceInProgress, - cdiv1.Paused: - return virtv2.ImageProvisioning - case cdiv1.Succeeded: - return virtv2.ImageReady - case cdiv1.Failed: - return virtv2.ImageFailed - default: - return virtv2.ImageUnknown - } -} - -// ensurePodFinalizers adds protective finalizers on importer/uploader Pod and Service dependencies. -func (r *VMIReconciler) ensurePodFinalizers(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - if state.Pod != nil && controllerutil.AddFinalizer(state.Pod, virtv2.FinalizerPodProtection) { - if err := opts.Client.Update(ctx, state.Pod); err != nil { - return fmt.Errorf("error setting finalizer on a Pod %q: %w", state.Pod.Name, err) - } - } - if state.Service != nil && controllerutil.AddFinalizer(state.Service, virtv2.FinalizerServiceProtection) { - if err := opts.Client.Update(ctx, state.Service); err != nil { - return fmt.Errorf("error setting finalizer on a Service %q: %w", state.Service.Name, err) - } - } - if state.Ingress != nil && controllerutil.AddFinalizer(state.Ingress, virtv2.FinalizerIngressProtection) { - if err := opts.Client.Update(ctx, state.Ingress); err != nil { - return fmt.Errorf("error setting finalizer on a Ingress %q: %w", state.Ingress.Name, err) - } - } - - return nil -} - -// ensureDVFinalizers adds protective finalizers on DataVolume, PersistentVolumeClaim and PersistentVolume dependencies. -func (r *VMIReconciler) ensureDVFinalizers(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - if state.DV != nil { - // Ensure DV finalizer is set in case DV was created manually (take ownership of already existing object) - if controllerutil.AddFinalizer(state.DV, virtv2.FinalizerDVProtection) { - if err := opts.Client.Update(ctx, state.DV); err != nil { - return fmt.Errorf("error setting finalizer on a DV %q: %w", state.DV.Name, err) - } - } - } - if state.PVC != nil { - if controllerutil.AddFinalizer(state.PVC, virtv2.FinalizerPVCProtection) { - if err := opts.Client.Update(ctx, state.PVC); err != nil { - return fmt.Errorf("error setting finalizer on a PVC %q: %w", state.PVC.Name, err) - } - } - } - if state.PV != nil { - if controllerutil.AddFinalizer(state.PV, virtv2.FinalizerPVProtection) { - if err := opts.Client.Update(ctx, state.PV); err != nil { - return fmt.Errorf("error setting finalizer on a PV %q: %w", state.PV.Name, err) - } - } - } - - return nil -} - -func (r *VMIReconciler) ShouldDeleteChildResources(state *VMIReconcilerState) bool { - return state.Pod != nil || state.Service != nil || state.Ingress != nil || state.PV != nil || state.PVC != nil || state.DV != nil -} - -func (r *VMIReconciler) setPVCOwnerReference(ctx context.Context, state *VMIReconcilerState, apiClient client.Client) error { - if state.PVC == nil { - return nil - } - - state.PVC.OwnerReferences = []v1.OwnerReference{ - *v1.NewControllerRef(state.VMI.Current(), state.VMI.Current().GroupVersionKind()), - } - - err := apiClient.Update(ctx, state.PVC) - if err != nil { - return fmt.Errorf("failed to set pvc owner ref: %w", err) - } - - return nil -} - -// removeFinalizerChildResources removes protective finalizers on Pod, Service, DataVolume, PersistentVolumeClaim and PersistentVolume dependencies. -func (r *VMIReconciler) removeFinalizerChildResources(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - if state.Pod != nil && controllerutil.RemoveFinalizer(state.Pod, virtv2.FinalizerPodProtection) { - if err := opts.Client.Update(ctx, state.Pod); err != nil { - return fmt.Errorf("unable to remove Pod %q finalizer %q: %w", state.Pod.Name, virtv2.FinalizerPodProtection, err) - } - } - if state.Service != nil && controllerutil.RemoveFinalizer(state.Service, virtv2.FinalizerServiceProtection) { - if err := opts.Client.Update(ctx, state.Service); err != nil { - return fmt.Errorf("unable to remove Service %q finalizer %q: %w", state.Service.Name, virtv2.FinalizerServiceProtection, err) - } - } - if state.Ingress != nil && controllerutil.RemoveFinalizer(state.Ingress, virtv2.FinalizerIngressProtection) { - if err := opts.Client.Update(ctx, state.Ingress); err != nil { - return fmt.Errorf("unable to remove Ingress %q finalizer %q: %w", state.Ingress.Name, virtv2.FinalizerIngressProtection, err) - } - } - if state.PV != nil && controllerutil.RemoveFinalizer(state.PV, virtv2.FinalizerPVProtection) { - if err := opts.Client.Update(ctx, state.PV); err != nil { - return fmt.Errorf("unable to remove PV %q finalizer %q: %w", state.PV.Name, virtv2.FinalizerPVProtection, err) - } - } - if state.PVC != nil && controllerutil.RemoveFinalizer(state.PVC, virtv2.FinalizerPVCProtection) { - if err := opts.Client.Update(ctx, state.PVC); err != nil { - return fmt.Errorf("unable to remove PVC %q finalizer %q: %w", state.PVC.Name, virtv2.FinalizerPVCProtection, err) - } - } - if state.DV != nil && controllerutil.RemoveFinalizer(state.DV, virtv2.FinalizerDVProtection) { - if err := opts.Client.Update(ctx, state.DV); err != nil { - return fmt.Errorf("unable to remove DV %q finalizer %q: %w", state.DV.Name, virtv2.FinalizerDVProtection, err) - } - } - return nil -} - -func (r *VMIReconciler) verifyDataSource(state *VMIReconcilerState) error { - switch state.VMI.Current().Spec.DataSource.Type { - case virtv2.DataSourceTypeObjectRef: - return state.DVCRDataSource.Validate() - default: - return nil - } -} - -func (r *VMIReconciler) startPod(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - switch state.VMI.Current().Spec.DataSource.Type { - case virtv2.DataSourceTypeUpload: - if err := r.startUploaderPod(ctx, state, opts); err != nil { - return err - } - - if err := r.startUploaderService(ctx, state, opts); err != nil { - return err - } - if err := r.startUploaderIngress(ctx, state, opts); err != nil { - return err - } - default: - if err := r.startImporterPod(ctx, state, opts); err != nil { - return err - } - } - - return nil -} - -func (r *VMIReconciler) cleanup( - ctx context.Context, - vmi *virtv2.VirtualImage, - client client.Client, - state *VMIReconcilerState, - opts two_phase_reconciler.ReconcilerOptions, -) error { - opts.Log.V(1).Info("Import done, cleanup") - if state.DV != nil { - err := supplements.CleanupForDataVolume(ctx, client, state.Supplements, r.dvcrSettings) - if err != nil { - return fmt.Errorf("cleanup supplements for DataVolume: %w", err) - } - // TODO(future): take ownership on PVC and delete DataVolume. - // if err := client.Delete(ctx, state.DV); err != nil { - // return fmt.Errorf("cleanup DataVolume: %w", err) - // } - } - - switch vmi.Spec.DataSource.Type { - case virtv2.DataSourceTypeUpload: - if state.Ingress != nil { - if err := uploader.CleanupIngress(ctx, client, state.Ingress); err != nil { - return err - } - } - if state.Service != nil { - if err := uploader.CleanupService(ctx, client, state.Service); err != nil { - return err - } - } - if state.Pod != nil { - if err := uploader.CleanupPod(ctx, client, state.Pod); err != nil { - return err - } - } - default: - if state.Pod != nil { - if err := importer.CleanupPod(ctx, client, state.Pod); err != nil { - return err - } - } - } - - return nil -} - -func (r *VMIReconciler) cleanupOnDeletion(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - if err := r.removeFinalizerChildResources(ctx, state, opts); err != nil { - return err - } - if r.ShouldDeleteChildResources(state) { - if err := r.cleanup(ctx, state.VMI.Current(), opts.Client, state, opts); err != nil { - return err - } - - if state.DV != nil { - if err := helper.DeleteObject(ctx, opts.Client, state.DV); err != nil { - return err - } - } - - if state.PVC != nil { - if err := helper.DeleteObject(ctx, opts.Client, state.PVC); err != nil { - return err - } - } - - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - } - controllerutil.RemoveFinalizer(state.VMI.Changed(), virtv2.FinalizerVMICleanup) - return nil -} - -func (r *VMIReconciler) FilterAttachedVM(vm *virtv2.VirtualMachine) bool { - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind == virtv2.ImageDevice { - return true - } - } - - return false -} - -func (r *VMIReconciler) EnqueueFromAttachedVM(vm *virtv2.VirtualMachine) []reconcile.Request { - var requests []reconcile.Request - - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind != virtv2.ImageDevice { - continue - } - - requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ - Name: bda.Name, - Namespace: vm.Namespace, - }}) - } - - return requests -} diff --git a/images/virtualization-artifact/pkg/controller/vmi_reconciler_state.go b/images/virtualization-artifact/pkg/controller/vmi_reconciler_state.go deleted file mode 100644 index a16e95f83..000000000 --- a/images/virtualization-artifact/pkg/controller/vmi_reconciler_state.go +++ /dev/null @@ -1,294 +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" - netv1 "k8s.io/api/networking/v1" - "k8s.io/apimachinery/pkg/types" - cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - vmiutil "github.com/deckhouse/virtualization-controller/pkg/common/vmi" - cc "github.com/deckhouse/virtualization-controller/pkg/controller/common" - "github.com/deckhouse/virtualization-controller/pkg/controller/importer" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" - "github.com/deckhouse/virtualization-controller/pkg/controller/vmattachee" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type VMIReconcilerState struct { - *vmattachee.AttacheeState[*virtv2.VirtualImage, virtv2.VirtualImageStatus] - - Client client.Client - Supplements *supplements.Generator - Result *reconcile.Result - - VMI *helper.Resource[*virtv2.VirtualImage, virtv2.VirtualImageStatus] - DV *cdiv1.DataVolume - PVC *corev1.PersistentVolumeClaim - PV *corev1.PersistentVolume - Pod *corev1.Pod - Service *corev1.Service - Ingress *netv1.Ingress - DVCRDataSource *DVCRDataSource -} - -func NewVMIReconcilerState(name types.NamespacedName, log logr.Logger, client client.Client, cache cache.Cache) *VMIReconcilerState { - state := &VMIReconcilerState{ - Client: client, - VMI: helper.NewResource( - name, log, client, cache, - func() *virtv2.VirtualImage { return &virtv2.VirtualImage{} }, - func(obj *virtv2.VirtualImage) virtv2.VirtualImageStatus { return obj.Status }, - ), - } - - state.AttacheeState = vmattachee.NewAttacheeState( - state, - virtv2.FinalizerVIProtection, - state.VMI, - ) - return state -} - -func (state *VMIReconcilerState) ApplySync(ctx context.Context, _ logr.Logger) error { - if err := state.VMI.UpdateMeta(ctx); err != nil { - return fmt.Errorf("unable to update VI %q meta: %w", state.VMI.Name(), err) - } - return nil -} - -func (state *VMIReconcilerState) ApplyUpdateStatus(ctx context.Context, _ logr.Logger) error { - return state.VMI.UpdateStatus(ctx) -} - -func (state *VMIReconcilerState) SetReconcilerResult(result *reconcile.Result) { - state.Result = result -} - -func (state *VMIReconcilerState) GetReconcilerResult() *reconcile.Result { - return state.Result -} - -func (state *VMIReconcilerState) ShouldApplyUpdateStatus() bool { - return state.VMI.IsStatusChanged() -} - -func (state *VMIReconcilerState) Reload(ctx context.Context, req reconcile.Request, log logr.Logger, client client.Client) error { - err := state.VMI.Fetch(ctx) - if err != nil { - return fmt.Errorf("unable to get %q: %w", req.NamespacedName, err) - } - if state.VMI.IsEmpty() { - log.Info("Reconcile observe an absent VI: it may be deleted", "vi.name", req.Name, "vi.namespace", req.Namespace) - return nil - } - - state.Supplements = &supplements.Generator{ - Prefix: vmiShortName, - Name: state.VMI.Current().Name, - Namespace: state.VMI.Current().Namespace, - UID: state.VMI.Current().UID, - } - - switch state.VMI.Current().Spec.DataSource.Type { - case virtv2.DataSourceTypeUpload: - state.Pod, err = uploader.FindPod(ctx, client, state.Supplements) - if err != nil { - return err - } - - state.Service, err = uploader.FindService(ctx, client, state.Supplements) - if err != nil { - return err - } - - state.Ingress, err = uploader.FindIngress(ctx, client, state.Supplements) - if err != nil { - return err - } - default: - state.Pod, err = importer.FindPod(ctx, client, state.Supplements) - if err != nil { - return err - } - - // TODO These resources are not part of the state. Retrieve additional resources in Sync phase. - state.DVCRDataSource, err = NewDVCRDataSourcesForVMI(ctx, state.VMI.Current().Spec.DataSource, state.VMI.Current(), client) - if err != nil { - return err - } - } - - dvName := state.Supplements.DataVolume() - state.DV, err = helper.FetchObject(ctx, dvName, client, &cdiv1.DataVolume{}) - if err != nil { - return fmt.Errorf("unable to get DV %q: %w", dvName, err) - } - - pvcName := dvName - state.PVC, err = helper.FetchObject(ctx, pvcName, client, &corev1.PersistentVolumeClaim{}) - if err != nil { - return fmt.Errorf("unable to get PVC %q: %w", pvcName, err) - } - - if state.PVC != nil && state.PVC.Status.Phase == corev1.ClaimBound { - pvName := types.NamespacedName{Name: state.PVC.Spec.VolumeName, Namespace: state.PVC.Namespace} - state.PV, err = helper.FetchObject(ctx, pvName, client, &corev1.PersistentVolume{}) - if err != nil { - return fmt.Errorf("unable to get PV %q: %w", pvName, err) - } - if state.PV == nil { - return fmt.Errorf("no PV %q found: expected existing PV for PVC %q in phase %q", pvName, state.PVC.Name, state.PVC.Status.Phase) - } - } - - return state.AttacheeState.Reload(ctx, req, log, client) -} - -func (state *VMIReconcilerState) ShouldReconcile(log logr.Logger) bool { - if state.VMI.IsEmpty() { - return false - } - - if state.AttacheeState.ShouldReconcile(log) { - return true - } - - return true -} - -func (state *VMIReconcilerState) IsProtected() bool { - return controllerutil.ContainsFinalizer(state.VMI.Current(), virtv2.FinalizerVMICleanup) -} - -func (state *VMIReconcilerState) IsLost() bool { - if state.VMI.IsEmpty() { - return false - } - return state.VMI.Current().Status.Phase == virtv2.ImagePVCLost -} - -func (state *VMIReconcilerState) IsReady() bool { - if state.VMI.IsEmpty() { - return false - } - return state.VMI.Current().Status.Phase == virtv2.ImageReady -} - -func (state *VMIReconcilerState) IsFailed() bool { - if state.VMI.IsEmpty() { - return false - } - return state.VMI.Current().Status.Phase == virtv2.ImageFailed -} - -func (state *VMIReconcilerState) IsDeletion() bool { - if state.VMI.IsEmpty() { - return false - } - return state.VMI.Current().DeletionTimestamp != nil -} - -func (state *VMIReconcilerState) ShouldTrackPod() bool { - if state.VMI.IsEmpty() { - return false - } - - // Always run importer Pod when storage is DVCR. - if state.VMI.Current().Spec.Storage == virtv2.StorageContainerRegistry { - return true - } - - // Run importer Pod for 2 phase import process (HTTP, Upload and ContainerImage sources). - return vmiutil.IsTwoPhaseImport(state.VMI.Current()) -} - -// CanStartPod returns whether importer/uploader Pod can be started. -// NOTE: valid only if ShouldTrackPod is true. -func (state *VMIReconcilerState) CanStartPod() bool { - return !state.IsReady() && !state.IsFailed() && state.Pod == nil -} - -// IsPodComplete returns whether importer/uploader Pod was completed. -// NOTE: valid only if ShouldTrackPod is true. -func (state *VMIReconcilerState) IsPodComplete() bool { - return state.Pod != nil && cc.IsPodComplete(state.Pod) -} - -// IsPodInProgress returns whether Pod is in progress. -// NOTE: valid only if ShouldTrackPod is true. -func (state *VMIReconcilerState) IsPodInProgress() bool { - return state.Pod != nil && state.Pod.Status.Phase == corev1.PodRunning -} - -func (state *VMIReconcilerState) HasTargetPVCSize() bool { - return state.GetTargetPVCSize() != "" -} - -func (state *VMIReconcilerState) GetTargetPVCSize() string { - if state.VMI.IsEmpty() { - return "" - } - - return state.VMI.Current().Status.Size.UnpackedBytes -} - -// ShouldTrackDataVolume returns true if import should be done via DataVolume. -func (state *VMIReconcilerState) ShouldTrackDataVolume() bool { - if state.VMI.IsEmpty() { - return false - } - - return state.VMI.Current().Spec.Storage == virtv2.StorageKubernetes -} - -func (state *VMIReconcilerState) CanCreateDataVolume() bool { - return state.DV == nil && !state.IsReady() -} - -func (state *VMIReconcilerState) IsDataVolumeInProgress() bool { - return state.DV != nil && state.DV.Status.Phase != cdiv1.Succeeded -} - -func (state *VMIReconcilerState) IsDataVolumeComplete() bool { - return state.DV != nil && state.DV.Status.Phase == cdiv1.Succeeded -} - -func (state *VMIReconcilerState) IsAttachedToVM(vm virtv2.VirtualMachine) bool { - if state.VMI.IsEmpty() { - return false - } - - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind == virtv2.ImageDevice && bda.Name == state.VMI.Name().Name { - return true - } - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/vmi_uploader.go b/images/virtualization-artifact/pkg/controller/vmi_uploader.go deleted file mode 100644 index 21f07e3ab..000000000 --- a/images/virtualization-artifact/pkg/controller/vmi_uploader.go +++ /dev/null @@ -1,144 +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/deckhouse/virtualization-controller/pkg/common/datasource" - vmiutil "github.com/deckhouse/virtualization-controller/pkg/common/vmi" - cc "github.com/deckhouse/virtualization-controller/pkg/controller/common" - "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" - "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" -) - -func (r *VMIReconciler) startUploaderPod(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - vmi := state.VMI.Current() - - opts.Log.V(1).Info("Creating uploader POD for VI", "vi.Name", vmi.Name) - - uploaderSettings := r.createUploaderSettings(state) - - podSettings := r.createUploaderPodSettings(state) - - uploaderPod := uploader.NewPod(podSettings, uploaderSettings) - - pod, err := uploaderPod.Create(ctx, opts.Client) - if err != nil { - err = cc.PublishPodErr(err, podSettings.Name, vmi, opts.Recorder, opts.Client) - if err != nil { - return err - } - } - - opts.Log.V(1).Info("Created uploader POD", "pod.Name", pod.Name) - - // Ensure supplement resources for the Pod. - return supplements.EnsureForPod(ctx, opts.Client, state.Supplements, pod, datasource.NewCABundleForVMI(vmi.Spec.DataSource), r.dvcrSettings) -} - -// createUploaderSettings fills settings for the dvcr-uploader binary. -func (r *VMIReconciler) createUploaderSettings(state *VMIReconcilerState) *uploader.Settings { - vmi := state.VMI.Current() - settings := &uploader.Settings{ - Verbose: r.verbose, - } - - // Set DVCR destination settings. - dvcrDestImageName := r.dvcrSettings.RegistryImageForVMI(vmi.Name, vmi.Namespace) - uploader.ApplyDVCRDestinationSettings(settings, r.dvcrSettings, state.Supplements, dvcrDestImageName) - - // TODO Update proxy settings. - - return settings -} - -func (r *VMIReconciler) createUploaderPodSettings(state *VMIReconcilerState) *uploader.PodSettings { - uploaderPod := state.Supplements.UploaderPod() - uploaderSvc := state.Supplements.UploaderService() - return &uploader.PodSettings{ - Name: uploaderPod.Name, - Image: r.uploaderImage, - PullPolicy: r.pullPolicy, - Namespace: uploaderPod.Namespace, - OwnerReference: vmiutil.MakeOwnerReference(state.VMI.Current()), - ControllerName: vmiControllerName, - InstallerLabels: map[string]string{}, - ServiceName: uploaderSvc.Name, - } -} - -func (r *VMIReconciler) startUploaderService(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.V(1).Info("Creating uploader Service for VI", "vi.Name", state.VMI.Current().Name) - - uploaderService := uploader.NewService(r.createUploaderServiceSettings(state)) - - service, err := uploaderService.Create(ctx, opts.Client) - if err != nil { - return err - } - - opts.Log.V(1).Info("Created uploader Service", "service.Name", service.Name) - - return nil -} - -func (r *VMIReconciler) createUploaderServiceSettings(state *VMIReconcilerState) *uploader.ServiceSettings { - uploaderSvc := state.Supplements.UploaderService() - return &uploader.ServiceSettings{ - Name: uploaderSvc.Name, - Namespace: uploaderSvc.Namespace, - OwnerReference: vmiutil.MakeOwnerReference(state.VMI.Current()), - } -} - -func (r *VMIReconciler) startUploaderIngress(ctx context.Context, state *VMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.V(1).Info("Creating uploader Ingress for VI", "vi.Name", state.VMI.Current().Name) - - uploaderIng := uploader.NewIngress(r.createUploaderIngressSettings(state)) - - ing, err := uploaderIng.Create(ctx, opts.Client) - if err != nil { - return err - } - - opts.Log.V(1).Info("Created uploader Ingress", "ingress.Name", ing.Name) - return supplements.EnsureForIngress(ctx, state.Client, state.Supplements, ing, r.dvcrSettings) -} - -func (r *VMIReconciler) createUploaderIngressSettings(state *VMIReconcilerState) *uploader.IngressSettings { - uploaderIng := state.Supplements.UploaderIngress() - uploaderSvc := state.Supplements.UploaderService() - secretName := r.dvcrSettings.UploaderIngressSettings.TLSSecret - if supplements.ShouldCopyUploaderTLSSecret(r.dvcrSettings, state.Supplements) { - secretName = state.Supplements.UploaderTLSSecretForIngress().Name - } - var class *string - if c := r.dvcrSettings.UploaderIngressSettings.Class; c != "" { - class = &c - } - return &uploader.IngressSettings{ - Name: uploaderIng.Name, - Namespace: uploaderIng.Namespace, - Host: r.dvcrSettings.UploaderIngressSettings.Host, - TLSSecretName: secretName, - ServiceName: uploaderSvc.Name, - ClassName: class, - OwnerReference: vmiutil.MakeOwnerReference(state.VMI.Current()), - } -}