diff --git a/.mirrord/mirrord.json b/.mirrord/mirrord.json new file mode 100644 index 000000000..0d603334f --- /dev/null +++ b/.mirrord/mirrord.json @@ -0,0 +1,25 @@ +{ + "feature": { + "network": { + "incoming": "steal", + "outgoing": true + }, + "fs": { + "mode": "read", + "read_only": [ "^/tmp/", "^/var/" ] + }, + "env": true + }, + "agent": { + "communication_timeout": 6000, + "startup_timeout": 56000 + }, + "internal_proxy": { + "start_idle_timeout": 13000, + "idle_timeout": 1500 + }, + "target": { + "namespace": "d8-virtualization", + "path": "deployment/virtualization-controller/container/virtualization-controller" + } +} diff --git a/api/core/v1alpha2/cluster_virtual_image.go b/api/core/v1alpha2/cluster_virtual_image.go index a7f231dd0..10ce40a4f 100644 --- a/api/core/v1alpha2/cluster_virtual_image.go +++ b/api/core/v1alpha2/cluster_virtual_image.go @@ -76,5 +76,7 @@ const ( ) type ClusterVirtualImageStatus struct { - ImageStatus `json:",inline"` + ImageStatus `json:",inline"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } diff --git a/api/core/v1alpha2/cvicondition/condition.go b/api/core/v1alpha2/cvicondition/condition.go new file mode 100644 index 000000000..9481e3012 --- /dev/null +++ b/api/core/v1alpha2/cvicondition/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 cvicondition + +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/data_source.go b/api/core/v1alpha2/data_source.go index 720b5da03..027d1c3dd 100644 --- a/api/core/v1alpha2/data_source.go +++ b/api/core/v1alpha2/data_source.go @@ -41,10 +41,8 @@ type Checksum struct { type DataSourceType string const ( - DataSourceTypeHTTP DataSourceType = "HTTP" - DataSourceTypeContainerImage DataSourceType = "ContainerImage" - DataSourceTypeObjectRef DataSourceType = "ObjectRef" - DataSourceTypeUpload DataSourceType = "Upload" - DataSourceTypeVirtualDiskSnapshot DataSourceType = "VirtualDiskSnapshot" - DataSourceTypePersistentVolumeClaim DataSourceType = "PersistentVolumeClaim" + DataSourceTypeHTTP DataSourceType = "HTTP" + DataSourceTypeContainerImage DataSourceType = "ContainerImage" + DataSourceTypeObjectRef DataSourceType = "ObjectRef" + DataSourceTypeUpload DataSourceType = "Upload" ) diff --git a/api/core/v1alpha2/finalizers.go b/api/core/v1alpha2/finalizers.go index 37c611061..8d1c89057 100644 --- a/api/core/v1alpha2/finalizers.go +++ b/api/core/v1alpha2/finalizers.go @@ -17,13 +17,15 @@ limitations under the License. package v1alpha2 const ( - FinalizerPodProtection = "virtualization.deckhouse.io/pod-protection" - FinalizerServiceProtection = "virtualization.deckhouse.io/svc-protection" - FinalizerIngressProtection = "virtualization.deckhouse.io/ingress-protection" - FinalizerSecretProtection = "virtualization.deckhouse.io/secret-protection" - FinalizerDVProtection = "virtualization.deckhouse.io/dv-protection" - FinalizerPVCProtection = "virtualization.deckhouse.io/pvc-protection" - FinalizerPVProtection = "virtualization.deckhouse.io/pv-protection" + FinalizerClusterVirtualImageProtection = "virtualization.deckhouse.io/cvi-protection" + FinalizerVirtualDiskProtection = "virtualization.deckhouse.io/vd-protection" + FinalizerPodProtection = "virtualization.deckhouse.io/pod-protection" + FinalizerServiceProtection = "virtualization.deckhouse.io/svc-protection" + FinalizerIngressProtection = "virtualization.deckhouse.io/ingress-protection" + FinalizerSecretProtection = "virtualization.deckhouse.io/secret-protection" + FinalizerDVProtection = "virtualization.deckhouse.io/dv-protection" + FinalizerPVCProtection = "virtualization.deckhouse.io/pvc-protection" + FinalizerPVProtection = "virtualization.deckhouse.io/pv-protection" FinalizerCVMIProtection = "virtualization.deckhouse.io/cvi-protection" FinalizerVMIProtection = "virtualization.deckhouse.io/vi-protection" @@ -32,9 +34,9 @@ const ( FinalizerVMOPProtection = "virtualization.deckhouse.io/vmop-protection" FinalizerVMCPUProtection = "virtualization.deckhouse.io/vmcpu-protection" - FinalizerCVMICleanup = "virtualization.deckhouse.io/cvi-cleanup" + FinalizerCVICleanup = "virtualization.deckhouse.io/cvi-cleanup" + FinalizerVDCleanup = "virtualization.deckhouse.io/vd-cleanup" FinalizerVMICleanup = "virtualization.deckhouse.io/vi-cleanup" - FinalizerVMDCleanup = "virtualization.deckhouse.io/vd-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 6c3b2baba..33c9c9cc3 100644 --- a/api/core/v1alpha2/image_status.go +++ b/api/core/v1alpha2/image_status.go @@ -26,21 +26,30 @@ const ( ImageFailed ImagePhase = "Failed" ImagePVCLost ImagePhase = "PVCLost" ImageUnknown ImagePhase = "Unknown" + ImageTerminating ImagePhase = "Terminating" ) type ImageStatus struct { DownloadSpeed ImageStatusSpeed `json:"downloadSpeed"` Size ImageStatusSize `json:"size"` - Format string `json:"format"` - // FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim - Capacity string `json:"capacity,omitempty"` - CDROM bool `json:"cdrom"` - Target ImageStatusTarget `json:"target"` - Phase ImagePhase `json:"phase"` - Progress string `json:"progress,omitempty"` - UploadCommand string `json:"uploadCommand,omitempty"` - FailureReason string `json:"failureReason"` - FailureMessage string `json:"failureMessage"` + Format string `json:"format,omitempty"` + // FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim. + Capacity string `json:"capacity,omitempty"` + CDROM bool `json:"cdrom,omitempty"` + Target ImageStatusTarget `json:"target"` + 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 { + Avg string `json:"avg,omitempty"` + AvgBytes string `json:"avgBytes,omitempty"` + Current string `json:"current,omitempty"` + CurrentBytes string `json:"currentBytes,omitempty"` } type ImageStatusSpeed struct { @@ -51,14 +60,14 @@ type ImageStatusSpeed struct { } type ImageStatusSize struct { - Stored string `json:"stored"` - StoredBytes string `json:"storedBytes"` - Unpacked string `json:"unpacked"` - UnpackedBytes string `json:"unpackedBytes"` + Stored string `json:"stored,omitempty"` + StoredBytes string `json:"storedBytes,omitempty"` + Unpacked string `json:"unpacked,omitempty"` + UnpackedBytes string `json:"unpackedBytes,omitempty"` } type ImageStatusTarget struct { - RegistryURL string `json:"registryURL"` + RegistryURL string `json:"registryURL,omitempty"` // FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim PersistentVolumeClaim string `json:"persistentVolumeClaimName,omitempty"` } diff --git a/api/core/v1alpha2/vdcondition/condition.go b/api/core/v1alpha2/vdcondition/condition.go new file mode 100644 index 000000000..c1ed3c6ee --- /dev/null +++ b/api/core/v1alpha2/vdcondition/condition.go @@ -0,0 +1,50 @@ +/* +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 vdcondition + +type Type = string + +const ( + DatasourceReadyType Type = "DatasourceReady" + ReadyType Type = "Ready" + ResizedType Type = "Resized" +) + +type ( + DatasourceReadyReason = string + ReadyReason = string + ResizedReason = 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" + ReadyReason_Lost ReadyReason = "PVCLost" + + ResizedReason_NotRequested ResizedReason = "NotRequested" + ResizedReason_InProgress ResizedReason = "InProgress" + ResizedReason_TooSmallDiskSize ResizedReason = "TooSmallDiskSize" + ResizedReason_Resized ResizedReason = "Resized" +) diff --git a/api/core/v1alpha2/virtual_disk.go b/api/core/v1alpha2/virtual_disk.go index 8e57fabda..97fe2cd37 100644 --- a/api/core/v1alpha2/virtual_disk.go +++ b/api/core/v1alpha2/virtual_disk.go @@ -43,14 +43,19 @@ type VirtualDiskSpec struct { } type VirtualDiskStatus struct { - DownloadSpeed VirtualDiskDownloadSpeed `json:"downloadSpeed"` - Capacity string `json:"capacity,omitempty"` - Target DiskTarget `json:"target"` - Progress string `json:"progress,omitempty"` - UploadCommand string `json:"uploadCommand,omitempty"` - Phase DiskPhase `json:"phase"` - FailureReason string `json:"failureReason"` - FailureMessage string `json:"failureMessage"` + DownloadSpeed VirtualDiskDownloadSpeed `json:"downloadSpeed"` + Capacity string `json:"capacity,omitempty"` + Target DiskTarget `json:"target"` + Progress string `json:"progress,omitempty"` + UploadCommand string `json:"uploadCommand,omitempty"` + Phase DiskPhase `json:"phase"` + AttachedToVirtualMachines []AttachedVirtualMachine `json:"attachedToVirtualMachines,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +type AttachedVirtualMachine struct { + Name string `json:"name"` } type VirtualDiskDataSource struct { @@ -80,7 +85,7 @@ type VirtualDiskDownloadSpeed struct { } type DiskTarget struct { - PersistentVolumeClaim string `json:"persistentVolumeClaimName"` + PersistentVolumeClaim string `json:"persistentVolumeClaimName,omitempty"` } type VirtualDiskPersistentVolumeClaim struct { @@ -102,8 +107,9 @@ const ( DiskPending DiskPhase = "Pending" DiskWaitForUserUpload DiskPhase = "WaitForUserUpload" DiskProvisioning DiskPhase = "Provisioning" - DiskReady DiskPhase = "Ready" DiskFailed DiskPhase = "Failed" - DiskPVCLost DiskPhase = "PVCLost" - DiskUnknown DiskPhase = "Unknown" + DiskLost DiskPhase = "Lost" + DiskReady DiskPhase = "Ready" + DiskResizing DiskPhase = "Resizing" + DiskTerminating DiskPhase = "Terminating" ) diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index fb1ae21e0..d6ec91892 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -21,12 +21,28 @@ limitations under the License. package v1alpha2 import ( - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AttachedVirtualMachine) DeepCopyInto(out *AttachedVirtualMachine) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AttachedVirtualMachine. +func (in *AttachedVirtualMachine) DeepCopy() *AttachedVirtualMachine { + if in == nil { + return nil + } + out := new(AttachedVirtualMachine) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BlockDeviceSpecRef) DeepCopyInto(out *BlockDeviceSpecRef) { *out = *in @@ -97,7 +113,7 @@ func (in *ClusterVirtualImage) DeepCopyInto(out *ClusterVirtualImage) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -220,6 +236,13 @@ func (in *ClusterVirtualImageSpec) DeepCopy() *ClusterVirtualImageSpec { func (in *ClusterVirtualImageStatus) DeepCopyInto(out *ClusterVirtualImageStatus) { *out = *in out.ImageStatus = in.ImageStatus + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -438,6 +461,22 @@ func (in *Provisioning) DeepCopy() *Provisioning { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StatusSpeed) DeepCopyInto(out *StatusSpeed) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusSpeed. +func (in *StatusSpeed) DeepCopy() *StatusSpeed { + if in == nil { + return nil + } + out := new(StatusSpeed) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SysprepRef) DeepCopyInto(out *SysprepRef) { *out = *in @@ -475,7 +514,7 @@ func (in *VMAffinity) DeepCopyInto(out *VMAffinity) { *out = *in if in.NodeAffinity != nil { in, out := &in.NodeAffinity, &out.NodeAffinity - *out = new(v1.NodeAffinity) + *out = new(corev1.NodeAffinity) (*in).DeepCopyInto(*out) } if in.VirtualMachineAndPodAffinity != nil { @@ -523,7 +562,7 @@ func (in *VirtualDisk) DeepCopyInto(out *VirtualDisk) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -694,6 +733,18 @@ func (in *VirtualDiskStatus) DeepCopyInto(out *VirtualDiskStatus) { *out = *in out.DownloadSpeed = in.DownloadSpeed out.Target = in.Target + if in.AttachedToVirtualMachines != nil { + in, out := &in.AttachedToVirtualMachines, &out.AttachedToVirtualMachines + *out = make([]AttachedVirtualMachine, len(*in)) + copy(*out, *in) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -934,7 +985,7 @@ func (in *VirtualMachineAndPodAffinityTerm) DeepCopyInto(out *VirtualMachineAndP *out = *in if in.LabelSelector != nil { in, out := &in.LabelSelector, &out.LabelSelector - *out = new(metav1.LabelSelector) + *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } if in.Namespaces != nil { @@ -944,7 +995,7 @@ func (in *VirtualMachineAndPodAffinityTerm) DeepCopyInto(out *VirtualMachineAndP } if in.NamespaceSelector != nil { in, out := &in.NamespaceSelector, &out.NamespaceSelector - *out = new(metav1.LabelSelector) + *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } if in.MatchLabelKeys != nil { @@ -1570,7 +1621,7 @@ func (in *VirtualMachineSpec) DeepCopyInto(out *VirtualMachineSpec) { *out = *in if in.TopologySpreadConstraints != nil { in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints - *out = make([]v1.TopologySpreadConstraint, len(*in)) + *out = make([]corev1.TopologySpreadConstraint, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1589,7 +1640,7 @@ func (in *VirtualMachineSpec) DeepCopyInto(out *VirtualMachineSpec) { } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations - *out = make([]v1.Toleration, len(*in)) + *out = make([]corev1.Toleration, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go index 0dd7fa98b..631d57548 100644 --- a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go +++ b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go @@ -32,6 +32,7 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ + "github.com/deckhouse/virtualization/api/core/v1alpha2.AttachedVirtualMachine": schema_virtualization_api_core_v1alpha2_AttachedVirtualMachine(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.BlockDeviceSpecRef": schema_virtualization_api_core_v1alpha2_BlockDeviceSpecRef(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.BlockDeviceStatusRef": schema_virtualization_api_core_v1alpha2_BlockDeviceStatusRef(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.CPUSpec": schema_virtualization_api_core_v1alpha2_CPUSpec(ref), @@ -53,6 +54,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/deckhouse/virtualization/api/core/v1alpha2.ImageStatusTarget": schema_virtualization_api_core_v1alpha2_ImageStatusTarget(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.MemorySpec": schema_virtualization_api_core_v1alpha2_MemorySpec(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.Provisioning": schema_virtualization_api_core_v1alpha2_Provisioning(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.StatusSpeed": schema_virtualization_api_core_v1alpha2_StatusSpeed(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.SysprepRef": schema_virtualization_api_core_v1alpha2_SysprepRef(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.UserDataRef": schema_virtualization_api_core_v1alpha2_UserDataRef(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VMAffinity": schema_virtualization_api_core_v1alpha2_VMAffinity(ref), @@ -626,6 +628,26 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA } } +func schema_virtualization_api_core_v1alpha2_AttachedVirtualMachine(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + func schema_virtualization_api_core_v1alpha2_BlockDeviceSpecRef(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -963,23 +985,21 @@ func schema_virtualization_api_core_v1alpha2_ClusterVirtualImageStatus(ref commo }, "format": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "capacity": { SchemaProps: spec.SchemaProps{ - Description: "FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim", + Description: "FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim.", Type: []string{"string"}, Format: "", }, }, "cdrom": { SchemaProps: spec.SchemaProps{ - Default: false, - Type: []string{"boolean"}, - Format: "", + Type: []string{"boolean"}, + Format: "", }, }, "target": { @@ -990,9 +1010,8 @@ func schema_virtualization_api_core_v1alpha2_ClusterVirtualImageStatus(ref commo }, "phase": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "progress": { @@ -1009,24 +1028,41 @@ func schema_virtualization_api_core_v1alpha2_ClusterVirtualImageStatus(ref commo }, "failureReason": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "failureMessage": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", + }, + }, + "conditions": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"), + }, + }, + }, + }, + }, + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int64", }, }, }, - Required: []string{"downloadSpeed", "size", "format", "cdrom", "target", "phase", "failureReason", "failureMessage"}, + Required: []string{"downloadSpeed", "size", "target"}, }, }, Dependencies: []string{ - "github.com/deckhouse/virtualization/api/core/v1alpha2.ImageStatusSize", "github.com/deckhouse/virtualization/api/core/v1alpha2.ImageStatusSpeed", "github.com/deckhouse/virtualization/api/core/v1alpha2.ImageStatusTarget"}, + "github.com/deckhouse/virtualization/api/core/v1alpha2.ImageStatusSize", "github.com/deckhouse/virtualization/api/core/v1alpha2.ImageStatusSpeed", "github.com/deckhouse/virtualization/api/core/v1alpha2.ImageStatusTarget", "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, } } @@ -1105,13 +1141,11 @@ func schema_virtualization_api_core_v1alpha2_DiskTarget(ref common.ReferenceCall Properties: map[string]spec.Schema{ "persistentVolumeClaimName": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"persistentVolumeClaimName"}, }, }, } @@ -1185,23 +1219,21 @@ func schema_virtualization_api_core_v1alpha2_ImageStatus(ref common.ReferenceCal }, "format": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "capacity": { SchemaProps: spec.SchemaProps{ - Description: "FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim", + Description: "FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim.", Type: []string{"string"}, Format: "", }, }, "cdrom": { SchemaProps: spec.SchemaProps{ - Default: false, - Type: []string{"boolean"}, - Format: "", + Type: []string{"boolean"}, + Format: "", }, }, "target": { @@ -1212,9 +1244,8 @@ func schema_virtualization_api_core_v1alpha2_ImageStatus(ref common.ReferenceCal }, "phase": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "progress": { @@ -1231,20 +1262,18 @@ func schema_virtualization_api_core_v1alpha2_ImageStatus(ref common.ReferenceCal }, "failureReason": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "failureMessage": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"downloadSpeed", "size", "format", "cdrom", "target", "phase", "failureReason", "failureMessage"}, + Required: []string{"downloadSpeed", "size", "target"}, }, }, Dependencies: []string{ @@ -1260,34 +1289,29 @@ func schema_virtualization_api_core_v1alpha2_ImageStatusSize(ref common.Referenc Properties: map[string]spec.Schema{ "stored": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "storedBytes": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "unpacked": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "unpackedBytes": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"stored", "storedBytes", "unpacked", "unpackedBytes"}, }, }, } @@ -1337,9 +1361,8 @@ func schema_virtualization_api_core_v1alpha2_ImageStatusTarget(ref common.Refere Properties: map[string]spec.Schema{ "registryURL": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "persistentVolumeClaimName": { @@ -1350,7 +1373,6 @@ func schema_virtualization_api_core_v1alpha2_ImageStatusTarget(ref common.Refere }, }, }, - Required: []string{"registryURL"}, }, }, } @@ -1414,6 +1436,42 @@ func schema_virtualization_api_core_v1alpha2_Provisioning(ref common.ReferenceCa } } +func schema_virtualization_api_core_v1alpha2_StatusSpeed(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "avg": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "avgBytes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "currentBytes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_virtualization_api_core_v1alpha2_SysprepRef(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1807,26 +1865,44 @@ func schema_virtualization_api_core_v1alpha2_VirtualDiskStatus(ref common.Refere Format: "", }, }, - "failureReason": { + "attachedToVirtualMachines": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/virtualization/api/core/v1alpha2.AttachedVirtualMachine"), + }, + }, + }, }, }, - "failureMessage": { + "conditions": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"), + }, + }, + }, + }, + }, + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int64", }, }, }, - Required: []string{"downloadSpeed", "target", "phase", "failureReason", "failureMessage"}, + Required: []string{"downloadSpeed", "target", "phase"}, }, }, Dependencies: []string{ - "github.com/deckhouse/virtualization/api/core/v1alpha2.DiskTarget", "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskDownloadSpeed"}, + "github.com/deckhouse/virtualization/api/core/v1alpha2.AttachedVirtualMachine", "github.com/deckhouse/virtualization/api/core/v1alpha2.DiskTarget", "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskDownloadSpeed", "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, } } @@ -2059,23 +2135,21 @@ func schema_virtualization_api_core_v1alpha2_VirtualImageStatus(ref common.Refer }, "format": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "capacity": { SchemaProps: spec.SchemaProps{ - Description: "FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim", + Description: "FIXME: create ClusterImageStatus without Capacity and PersistentVolumeClaim.", Type: []string{"string"}, Format: "", }, }, "cdrom": { SchemaProps: spec.SchemaProps{ - Default: false, - Type: []string{"boolean"}, - Format: "", + Type: []string{"boolean"}, + Format: "", }, }, "target": { @@ -2086,9 +2160,8 @@ func schema_virtualization_api_core_v1alpha2_VirtualImageStatus(ref common.Refer }, "phase": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "progress": { @@ -2105,20 +2178,18 @@ func schema_virtualization_api_core_v1alpha2_VirtualImageStatus(ref common.Refer }, "failureReason": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, "failureMessage": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"downloadSpeed", "size", "format", "cdrom", "target", "phase", "failureReason", "failureMessage"}, + Required: []string{"downloadSpeed", "size", "target"}, }, }, Dependencies: []string{ diff --git a/crds/clustervirtualimage.yaml b/crds/clustervirtualimage.yaml index 9db238ae2..44c44efd3 100644 --- a/crds/clustervirtualimage.yaml +++ b/crds/clustervirtualimage.yaml @@ -94,12 +94,6 @@ spec: 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: | @@ -193,6 +187,32 @@ spec: status: type: object properties: + conditions: + type: array + description: | + Hold the state information of the `ClusterVirtualImage`. + items: + type: object + properties: + type: + type: string + status: + type: string + reason: + type: string + message: + type: string + lastProbeTime: + type: string + format: date-time + nullable: true + lastTransitionTime: + type: string + format: date-time + nullable: true + required: + - type + - status downloadSpeed: type: object description: | @@ -270,6 +290,7 @@ spec: * 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. + * Terminating - The process of resource deletion is in progress. enum: [ "Pending", @@ -278,6 +299,7 @@ spec: "Ready", "Failed", "ImageLost", + "Terminating", "Unknown", ] progress: @@ -288,31 +310,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 + observedGeneration: + type: integer description: | - Detailed description of the error. human-readable - attachedToVirtualMachines: - type: array - description: | - A list of virtual machines and namespaces that use the image - example: - [ - { name: "VM100", namespace: "customer1" }, - { name: "VM200", namespace: "customer1" }, - { name: "VM200", namespace: "customer2" }, - ] - items: - type: object - properties: - name: - type: string - namespace: - type: string + Represents the .metadata.generation that the status was set based upon. additionalPrinterColumns: - name: Phase type: string diff --git a/crds/doc-ru-clustervirtualimage.yaml b/crds/doc-ru-clustervirtualimage.yaml index c678a9c4e..7feef3d4d 100644 --- a/crds/doc-ru-clustervirtualimage.yaml +++ b/crds/doc-ru-clustervirtualimage.yaml @@ -95,9 +95,9 @@ spec: * `Upload` — загрузить образ вручную, через веб-интерфейс. status: properties: - attachedToVirtualMachines: + conditions: description: | - Список виртуальных машин, использующих этот образ + Содержит информацию о состоянии `ClusterVirtualImage`. cdrom: description: | Является ли образ форматом, который должен быть смонтирован как cdrom, например iso и т. д. @@ -117,12 +117,6 @@ spec: currentBytes: description: | Текущая скорость загрузки в байтах в секунду. - failureMessage: - description: | - Подробное описание ошибки. - failureReason: - description: | - Краткое описание причины ошибки. format: description: | Обнаруженный формат образа. @@ -137,6 +131,7 @@ spec: * Failed - При создании ресурса возникла проблема, подробности можно увидеть в `.status.failureReason` и `.status.failureMessage`. * NotReady - Невозможно получить информацию о дочернем образе из-за невозможности подключения к DVCR. Ресурс не может быть использован. * ImageLost - Дочернее образ ресурса отсутствует. Ресурс не может быть использован. + * Terminating - Ресурс находится в процессе удаления. progress: description: | Ход копирования образа из источника в DVCR. Отображается только на этапе `Provisioning`. @@ -163,3 +158,6 @@ spec: uploadCommand: description: | Команда для загрузки образа для типа 'Upload'. + observedGeneration: + description: | + Представляет .metadata.generation, на основе которого был установлен статус. diff --git a/crds/doc-ru-virtualdisk.yaml b/crds/doc-ru-virtualdisk.yaml index 7f6e66f87..daa4dfe3b 100644 --- a/crds/doc-ru-virtualdisk.yaml +++ b/crds/doc-ru-virtualdisk.yaml @@ -49,9 +49,6 @@ spec: description: "" sha256: description: "" - insecureSkipVerify: - description: | - Отключить проверку TLS-сертификата (небезопасно и не должно использоваться в производственных средах). url: description: | URL с образом. Поддерживаются следующие типы образов: @@ -95,6 +92,9 @@ spec: Имя класса StorageClass, требуемого для PersistentVolumeClaim. Дополнительная информация — https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1. status: properties: + conditions: + description: | + Содержит информацию о состоянии `VirtualDisk`. attachedToVirtualMachines: description: | Список виртуальных машин, использующих этот диск @@ -117,12 +117,6 @@ spec: currentBytes: description: | Текущая скорость загрузки в байтах в секунду. - failureMessage: - description: | - Подробное описание ошибки. - failureReason: - description: | - Краткое описание причины ошибки. phase: description: | Текущее состояние ресурса `VirtualDisk`: @@ -133,6 +127,7 @@ spec: * Ready — ресурс создан и готов к использованию. * Failed — при создании ресурса возникла проблема, подробности можно увидеть в `.status.failureReason` и `.status.failureMessage`. * PVCLost — дочерний PVC ресурса отсутствует. Ресурс не может быть использован. + * Terminating - Ресурс находится в процессе удаления. progress: description: | Ход копирования образа из источника в DVCR. Отображается только на этапе `Provisioning`. @@ -144,3 +139,6 @@ spec: uploadCommand: description: | Команда для загрузки образа для типа 'Upload'. + observedGeneration: + description: | + Представляет .metadata.generation, на основе которого был установлен статус. diff --git a/crds/virtualdisk.yaml b/crds/virtualdisk.yaml index e740f9d0d..89da6d961 100644 --- a/crds/virtualdisk.yaml +++ b/crds/virtualdisk.yaml @@ -107,12 +107,6 @@ spec: 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,32 @@ spec: status: type: object properties: + conditions: + type: array + description: | + Hold the state information of the `ClusterVirtualImage`. + items: + type: object + properties: + type: + type: string + status: + type: string + reason: + type: string + message: + type: string + lastProbeTime: + type: string + format: date-time + nullable: true + lastTransitionTime: + type: string + format: date-time + nullable: true + required: + - type + - status downloadSpeed: type: object description: | @@ -242,6 +262,7 @@ spec: * 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`. * PVCLost - The child PVC of the resource is missing. The resource cannot be used. + * Terminating - The process of resource deletion is in progress. enum: [ "Pending", @@ -251,6 +272,7 @@ spec: "Failed", "PVCLost", "Unknown", + "Terminating", ] progress: type: string @@ -260,14 +282,6 @@ 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 description: | @@ -278,6 +292,10 @@ spec: properties: name: type: string + observedGeneration: + type: integer + description: | + Represents the .metadata.generation that the status was set based upon. additionalPrinterColumns: - name: Phase type: string diff --git a/images/dvcr-artifact/pkg/errors/errors.go b/images/dvcr-artifact/pkg/errors/errors.go new file mode 100644 index 000000000..cb2ada769 --- /dev/null +++ b/images/dvcr-artifact/pkg/errors/errors.go @@ -0,0 +1,46 @@ +/* +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 errors + +import "fmt" + +type ReasonError interface { + error + Reason() string +} + +func NewBadImageChecksumError(expectedSum, actualSum, algorithm string) BadImageChecksumError { + return BadImageChecksumError{ + algorithm: algorithm, + expected: expectedSum, + actual: actualSum, + } +} + +type BadImageChecksumError struct { + expected string + actual string + algorithm string +} + +func (e BadImageChecksumError) Reason() string { + return "BadImageChecksum" +} + +func (e BadImageChecksumError) Error() string { + return fmt.Sprintf("%s sum mismatch: %s != %s", e.algorithm, e.expected, e.actual) +} diff --git a/images/dvcr-artifact/pkg/monitoring/termination_message.go b/images/dvcr-artifact/pkg/monitoring/termination_message.go index e6744e859..8a40ec6e3 100644 --- a/images/dvcr-artifact/pkg/monitoring/termination_message.go +++ b/images/dvcr-artifact/pkg/monitoring/termination_message.go @@ -20,6 +20,7 @@ import ( "encoding/json" "errors" "fmt" + "k8s.io/klog/v2" "kubevirt.io/containerized-data-importer/pkg/util" ) diff --git a/images/dvcr-artifact/pkg/registry/registry.go b/images/dvcr-artifact/pkg/registry/registry.go index 45be2010f..abdc60be5 100644 --- a/images/dvcr-artifact/pkg/registry/registry.go +++ b/images/dvcr-artifact/pkg/registry/registry.go @@ -47,6 +47,7 @@ import ( "kubevirt.io/containerized-data-importer/pkg/util" "github.com/deckhouse/virtualization-controller/dvcr-importers/pkg/datasource" + importerrs "github.com/deckhouse/virtualization-controller/dvcr-importers/pkg/errors" "github.com/deckhouse/virtualization-controller/dvcr-importers/pkg/monitoring" ) @@ -195,7 +196,7 @@ func (p DataProcessor) inspectAndStreamSourceImage( checksumCheckFuncList = append(checksumCheckFuncList, func() error { sum := hex.EncodeToString(hash.Sum(nil)) if sum != p.sha256Sum { - return fmt.Errorf("sha256 sum mismatch: %s != %s", sum, p.sha256Sum) + return importerrs.NewBadImageChecksumError("sha256", p.sha256Sum, sum) } return nil @@ -208,7 +209,7 @@ func (p DataProcessor) inspectAndStreamSourceImage( checksumCheckFuncList = append(checksumCheckFuncList, func() error { sum := hex.EncodeToString(hash.Sum(nil)) if sum != p.md5Sum { - return fmt.Errorf("md5 sum mismatch: %s != %s", sum, p.md5Sum) + return importerrs.NewBadImageChecksumError("md5", p.md5Sum, sum) } return nil diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index ec721c3d2..f733f8496 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -41,6 +41,7 @@ import ( appconfig "github.com/deckhouse/virtualization-controller/pkg/config" "github.com/deckhouse/virtualization-controller/pkg/controller" "github.com/deckhouse/virtualization-controller/pkg/controller/cpu" + "github.com/deckhouse/virtualization-controller/pkg/controller/cvi" "github.com/deckhouse/virtualization-controller/pkg/controller/ipam" "github.com/deckhouse/virtualization-controller/pkg/controller/vmop" virtv2alpha1 "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -126,6 +127,7 @@ func main() { log.Error(err, "") os.Exit(1) } + // Override content type to JSON so proxy can rewrite payloads. cfg.ContentType = apiruntime.ContentTypeJSON @@ -137,7 +139,7 @@ func main() { // Setup scheme for all resources scheme := apiruntime.NewScheme() for _, f := range resourcesSchemeFuncs { - err := f(scheme) + err = f(scheme) if err != nil { log.Error(err, "Failed to add to scheme") os.Exit(1) @@ -172,12 +174,7 @@ func main() { // Setup context to gracefully handle termination. ctx := signals.SetupSignalHandler() - if _, err := controller.NewVMDController(ctx, mgr, log, importerImage, uploaderImage, dvcrSettings); err != nil { - log.Error(err, "") - os.Exit(1) - } - - if _, err := controller.NewCVMIController(ctx, mgr, log, importerImage, uploaderImage, controllerNamespace, dvcrSettings); err != nil { + if _, err = cvi.NewController(ctx, mgr, log, importerImage, uploaderImage, dvcrSettings, controllerNamespace); err != nil { log.Error(err, "") os.Exit(1) } diff --git a/images/virtualization-artifact/pkg/common/cvmi/util.go b/images/virtualization-artifact/pkg/common/cvmi/util.go deleted file mode 100644 index 6d0f2f864..000000000 --- a/images/virtualization-artifact/pkg/common/cvmi/util.go +++ /dev/null @@ -1,41 +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 cvmi - -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(cvmi *virtv2alpha1.ClusterVirtualImage) metav1.OwnerReference { - blockOwnerDeletion := true - isController := true - return metav1.OwnerReference{ - APIVersion: virtv2alpha1.ClusterVirtualImageGVK.GroupVersion().String(), - Kind: virtv2alpha1.ClusterVirtualImageGVK.Kind, - Name: cvmi.Name, - UID: cvmi.GetUID(), - BlockOwnerDeletion: &blockOwnerDeletion, - Controller: &isController, - } -} - -func IsDVCRSource(cvmi *virtv2alpha1.ClusterVirtualImage) bool { - return cvmi != nil && cvmi.Spec.DataSource.Type == virtv2alpha1.DataSourceTypeObjectRef -} diff --git a/images/virtualization-artifact/pkg/controller/common/util.go b/images/virtualization-artifact/pkg/controller/common/util.go index d05223e51..9b01399a7 100644 --- a/images/virtualization-artifact/pkg/controller/common/util.go +++ b/images/virtualization-artifact/pkg/controller/common/util.go @@ -22,6 +22,7 @@ import ( "crypto/rsa" "errors" "fmt" + "reflect" "strings" "sync" "time" @@ -33,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/utils/ptr" + cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/deckhouse/virtualization-controller/pkg/common" @@ -40,6 +42,8 @@ import ( ) const ( + CVIShortName = "cvi" + // AnnAPIGroup is the APIGroup for virtualization-controller. AnnAPIGroup = "virt.deckhouse.io" @@ -359,20 +363,27 @@ func CreateCloneSourcePodName(obj UIDable) string { return string(obj.GetUID()) + common.ClonerSourcePodNameSuffix } -// IsPVCComplete returns true if a PVC is in 'Succeeded' phase, false if not -func IsPVCComplete(pvc *corev1.PersistentVolumeClaim) bool { - if pvc != nil { - phase, exists := pvc.ObjectMeta.Annotations[AnnPodPhase] - return exists && (phase == string(corev1.PodSucceeded)) - } - return false +// IsPodComplete returns true if a Pod is in 'Succeeded' phase, false if not. +func IsPodComplete(pod *corev1.Pod) bool { + return pod != nil && pod.Status.Phase == corev1.PodSucceeded } -// IsPodComplete returns true if a Pod is in 'Completed' phase, false if not. -func IsPodComplete(pod *corev1.Pod) bool { - if pod != nil { - return pod.Status.Phase == corev1.PodSucceeded +// IsDataVolumeComplete returns true if a DataVolume is in 'Succeeded' phase, false if not. +func IsDataVolumeComplete(dv *cdiv1.DataVolume) bool { + return dv != nil && dv.Status.Phase == cdiv1.Succeeded +} + +func IsTerminating(obj client.Object) bool { + return !reflect.ValueOf(obj).IsNil() && obj.GetDeletionTimestamp() != nil +} + +func AnyTerminating(objs ...client.Object) bool { + for _, obj := range objs { + if IsTerminating(obj) { + return true + } } + return false } diff --git a/images/virtualization-artifact/pkg/controller/controller_suite_test.go b/images/virtualization-artifact/pkg/controller/controller_suite_test.go index 9e935365e..0f9a309c7 100644 --- a/images/virtualization-artifact/pkg/controller/controller_suite_test.go +++ b/images/virtualization-artifact/pkg/controller/controller_suite_test.go @@ -47,42 +47,11 @@ func TestController(t *testing.T) { RunSpecs(t, "Controller Suite") } -var ( - vmdControllerLog = logf.Log.WithName("vmd-controller-test") - vmControllerLog = logf.Log.WithName("vm-controller-test") -) - type TestReconcilerOptions struct { KnownObjects []client.Object RuntimeObjects []runtime.Object } -func NewTestVMDReconciler(opts TestReconcilerOptions) *two_phase_reconciler.ReconcilerCore[*VMDReconcilerState] { - s := scheme.Scheme - _ = cdiv1.AddToScheme(s) - _ = metav1.AddMetaToScheme(s) - _ = v1alpha2.AddToScheme(s) - _ = virtv1.AddToScheme(s) - - builder := fake.NewClientBuilder(). - WithScheme(s). - WithStatusSubresource(opts.KnownObjects...). - WithRuntimeObjects(opts.RuntimeObjects...) - - cl := builder.Build() - rec := record.NewFakeRecorder(10) - - return two_phase_reconciler.NewReconcilerCore[*VMDReconcilerState]( - &VMDReconciler{}, - NewVMDReconcilerState, - two_phase_reconciler.ReconcilerOptions{ - Client: cl, - Recorder: rec, - Scheme: s, - Log: vmdControllerLog, - }) -} - func NewTestVMReconciler(opts TestReconcilerOptions) *two_phase_reconciler.ReconcilerCore[*VMReconcilerState] { s := scheme.Scheme _ = cdiv1.AddToScheme(s) diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go new file mode 100644 index 000000000..05d29da2c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go @@ -0,0 +1,93 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cvi + +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/cvi/internal" + "github.com/deckhouse/virtualization-controller/pkg/controller/cvi/internal/source" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +const ( + ControllerName = "cvi-controller" + + PodVerbose = "3" + PodPullPolicy = string(corev1.PullIfNotPresent) +) + +type Condition interface { + Handle(ctx context.Context, cvi *virtv2.ClusterVirtualImage) error +} + +func NewController( + ctx context.Context, + mgr manager.Manager, + log logr.Logger, + importerImage string, + uploaderImage string, + dvcr *dvcr.Settings, + ns string, +) (controller.Controller, error) { + stat := service.NewStatService() + protection := service.NewProtectionService(mgr.GetClient(), virtv2.FinalizerClusterVirtualImageProtection) + 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, ns)) + sources.Set(virtv2.DataSourceTypeContainerImage, source.NewRegistryDataSource(stat, importer, dvcr, mgr.GetClient(), ns)) + sources.Set(virtv2.DataSourceTypeObjectRef, source.NewObjectRefDataSource(stat, importer, dvcr, mgr.GetClient(), ns)) + sources.Set(virtv2.DataSourceTypeUpload, source.NewUploadDataSource(stat, uploader, dvcr, ns)) + + reconciler := NewReconciler( + mgr.GetClient(), + internal.NewDatasourceReadyHandler(sources), + internal.NewLifeCycleHandler(sources, mgr.GetClient()), + internal.NewDeletionHandler(sources), + ) + + cviController, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: reconciler}) + if err != nil { + return nil, err + } + + err = reconciler.SetupController(ctx, mgr, cviController) + if err != nil { + return nil, err + } + + if err = builder.WebhookManagedBy(mgr). + For(&virtv2.ClusterVirtualImage{}). + WithValidator(NewValidator()). + Complete(); err != nil { + return nil, err + } + + log.Info("Initialized ClusterVirtualImage controller", "image", importerImage) + + return cviController, nil +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go new file mode 100644 index 000000000..4742b4e08 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go @@ -0,0 +1,193 @@ +/* +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 cvi + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Handler interface { + Handle(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (reconcile.Result, error) +} + +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) { + cvi := service.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) + + err := cvi.Fetch(ctx) + if err != nil { + return reconcile.Result{}, err + } + + if cvi.IsEmpty() { + return reconcile.Result{}, nil + } + + var requeue bool + + slog.Info("Start") + + var handlerErrs []error + + for _, h := range r.handlers { + slog.Info("Handle...") + var res reconcile.Result + res, err = h.Handle(ctx, cvi.Changed()) + if err != nil { + slog.Error("Failed to handle cvi", "err", err) + handlerErrs = append(handlerErrs, err) + } + + // TODO: merger. + requeue = requeue || res.Requeue + } + + cvi.Changed().Status.ObservedGeneration = cvi.Changed().Generation + + slog.Info("Update") + + err = cvi.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") + return reconcile.Result{ + RequeueAfter: 5 * time.Second, + }, nil + } + + slog.Info("Done") + + 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.ClusterVirtualImage{}), + &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 + } + + if err := ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualMachine{}), + handler.EnqueueRequestsFromMapFunc(r.enqueueClusterImagesAttachedToVM()), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return r.vmHasAttachedClusterImages(e.Object) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return r.vmHasAttachedClusterImages(e.Object) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return r.vmHasAttachedClusterImages(e.ObjectOld) || r.vmHasAttachedClusterImages(e.ObjectNew) + }, + }, + ); err != nil { + return fmt.Errorf("error setting watch on VMs: %w", err) + } + + return nil +} + +func (r *Reconciler) factory() *virtv2.ClusterVirtualImage { + return &virtv2.ClusterVirtualImage{} +} + +func (r *Reconciler) statusGetter(obj *virtv2.ClusterVirtualImage) virtv2.ClusterVirtualImageStatus { + return obj.Status +} + +func (r *Reconciler) enqueueClusterImagesAttachedToVM() handler.MapFunc { + return func(_ context.Context, obj client.Object) []reconcile.Request { + vm, ok := obj.(*virtv2.VirtualMachine) + if !ok { + return nil + } + + var requests []reconcile.Request + + for _, bda := range vm.Status.BlockDeviceRefs { + if bda.Kind != virtv2.ClusterImageDevice { + continue + } + + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ + Name: bda.Name, + }}) + } + + return requests + } +} + +func (r *Reconciler) vmHasAttachedClusterImages(obj client.Object) bool { + vm, ok := obj.(*virtv2.VirtualMachine) + if !ok { + return false + } + + for _, bda := range vm.Status.BlockDeviceRefs { + if bda.Kind == virtv2.ClusterImageDevice { + return true + } + } + + return false +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_webhook.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_webhook.go new file mode 100644 index 000000000..f466428e5 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_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 cvi + +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/common" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type Validator struct { + logger *slog.Logger +} + +func NewValidator() *Validator { + return &Validator{ + logger: slog.Default().With("controller", common.CVIShortName, "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) { + oldCVI, ok := oldObj.(*virtv2.ClusterVirtualImage) + if !ok { + return nil, fmt.Errorf("expected an old ClusterVirtualImage but got a %T", newObj) + } + + newCVI, ok := newObj.(*virtv2.ClusterVirtualImage) + if !ok { + return nil, fmt.Errorf("expected a new ClusterVirtualImage but got a %T", newObj) + } + + v.logger.Info("Validating ClusterVirtualImage") + + if oldCVI.Generation == newCVI.Generation { + return nil, nil + } + + ready, _ := service.GetCondition(cvicondition.ReadyType, newCVI.Status.Conditions) + if newCVI.Status.Phase == virtv2.ImageReady || ready.Status == metav1.ConditionTrue { + return nil, fmt.Errorf("ClusterVirtualImage is in a Ready state: configuration changes are not available") + } + + return nil, nil +} + +func (v *Validator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + err := fmt.Errorf("misconfigured webhook rules: delete operation not implemented") + v.logger.Error("Ensure the correctness of ValidatingWebhookConfiguration", "err", err) + return nil, nil +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/datasource_ready.go b/images/virtualization-artifact/pkg/controller/cvi/internal/datasource_ready.go new file mode 100644 index 000000000..cf8172883 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/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/cvi/internal/source" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type DatasourceReadyHandler struct { + sources *source.Sources +} + +func NewDatasourceReadyHandler(sources *source.Sources) *DatasourceReadyHandler { + return &DatasourceReadyHandler{ + sources: sources, + } +} + +func (h DatasourceReadyHandler) Handle(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (reconcile.Result, error) { + condition, ok := service.GetCondition(cvicondition.DatasourceReadyType, cvi.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: cvicondition.DatasourceReadyType, + Status: metav1.ConditionUnknown, + } + } + + defer func() { service.SetCondition(condition, &cvi.Status.Conditions) }() + + if cvi.DeletionTimestamp != nil { + return reconcile.Result{}, nil + } + + s, ok := h.sources.Get(cvi.Spec.DataSource.Type) + if !ok { + err := fmt.Errorf("data source validator not found for type: %s", cvi.Spec.DataSource.Type) + condition.Message = err.Error() + return reconcile.Result{}, err + } + + err := s.Validate(ctx, cvi) + switch { + case err == nil: + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.DatasourceReadyReason_DatasourceReady + condition.Message = "" + case errors.Is(err, source.ErrSecretNotFound): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.DatasourceReadyReason_ContainerRegistrySecretNotFound + condition.Message = strings.ToTitle(err.Error()) + case errors.As(err, &source.ImageNotReadyError{}): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.DatasourceReadyReason_ImageNotReady + condition.Message = strings.ToTitle(err.Error()) + case errors.As(err, &source.ClusterImageNotReadyError{}): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.DatasourceReadyReason_ClusterImageNotReady + condition.Message = strings.ToTitle(err.Error()) + } + + return reconcile.Result{}, err +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go b/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go new file mode 100644 index 000000000..297dcb35c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/deletion.go @@ -0,0 +1,56 @@ +/* +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/cvi/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, cvi *virtv2.ClusterVirtualImage) (reconcile.Result, error) { + if cvi.DeletionTimestamp != nil { + requeue, err := h.sources.CleanUp(ctx, cvi) + if err != nil { + return reconcile.Result{}, err + } + + if requeue { + return reconcile.Result{Requeue: true}, nil + } + + controllerutil.RemoveFinalizer(cvi, virtv2.FinalizerCVICleanup) + return reconcile.Result{}, nil + } + + controllerutil.AddFinalizer(cvi, virtv2.FinalizerCVICleanup) + return reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go new file mode 100644 index 000000000..4cc3b7b98 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/life_cycle.go @@ -0,0 +1,102 @@ +/* +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/cvi/internal/source" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +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, cvi *virtv2.ClusterVirtualImage) (reconcile.Result, error) { + readyCondition, ok := service.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + if !ok { + readyCondition = metav1.Condition{ + Type: cvicondition.ReadyType, + Status: metav1.ConditionUnknown, + } + + service.SetCondition(readyCondition, &cvi.Status.Conditions) + } + + if cvi.DeletionTimestamp != nil { + cvi.Status.Phase = virtv2.ImageTerminating + return reconcile.Result{}, nil + } + + if cvi.Status.Phase == "" { + cvi.Status.Phase = virtv2.ImagePending + } + + dataSourceReadyCondition, exists := service.GetCondition(cvicondition.DatasourceReadyType, cvi.Status.Conditions) + if !exists { + return reconcile.Result{}, fmt.Errorf("condition %s not found, but required", cvicondition.DatasourceReadyType) + } + + if dataSourceReadyCondition.Status != metav1.ConditionTrue { + return reconcile.Result{}, nil + } + + if readyCondition.Status != metav1.ConditionTrue && h.sources.Changed(ctx, cvi) { + cvi.Status = virtv2.ClusterVirtualImageStatus{ + ImageStatus: virtv2.ImageStatus{ + Phase: virtv2.ImagePending, + }, + Conditions: cvi.Status.Conditions, + ObservedGeneration: cvi.Status.ObservedGeneration, + } + + _, err := h.sources.CleanUp(ctx, cvi) + if err != nil { + return reconcile.Result{}, err + } + + return reconcile.Result{Requeue: true}, nil + } + + ds, exists := h.sources.Get(cvi.Spec.DataSource.Type) + if !exists { + return reconcile.Result{}, fmt.Errorf("data source runner not found for type: %s", cvi.Spec.DataSource.Type) + } + + requeue, err := ds.Sync(ctx, cvi) + if err != nil { + return reconcile.Result{}, err + } + + return reconcile.Result{Requeue: requeue}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/data_source_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/data_source_test.go new file mode 100644 index 000000000..4501d181c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/data_source_test.go @@ -0,0 +1,29 @@ +/* +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 ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestDataSources(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "DataSources") +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/errors.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/errors.go new file mode 100644 index 000000000..60c631bd0 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/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/cvi/internal/source/http.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/http.go new file mode 100644 index 000000000..cf040f271 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/http.go @@ -0,0 +1,190 @@ +/* +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" + "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/dvcr" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type HTTPDataSource struct { + statService Stat + importerService Importer + dvcrSettings *dvcr.Settings + controllerNamespace string + logger *slog.Logger +} + +func NewHTTPDataSource( + statService Stat, + importerService Importer, + dvcrSettings *dvcr.Settings, + controllerNamespace string, +) *HTTPDataSource { + return &HTTPDataSource{ + statService: statService, + importerService: importerService, + dvcrSettings: dvcrSettings, + controllerNamespace: controllerNamespace, + logger: slog.Default().With("controller", common.CVIShortName, "ds", "http"), + } +} + +func (ds HTTPDataSource) Sync(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + ds.logger.Info("Sync", "cvi", cvi.Name) + + condition, _ := service.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + defer func() { service.SetCondition(condition, &cvi.Status.Conditions) }() + + supgen := supplements.NewGenerator(common.CVIShortName, cvi.Name, ds.controllerNamespace, cvi.UID) + pod, err := ds.importerService.GetPod(ctx, supgen) + if err != nil { + return false, err + } + + switch { + case isDiskProvisioningFinished(condition): + ds.logger.Info("Finishing...", "cvi", cvi.Name) + + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.ReadyReason_Ready + condition.Message = "" + + cvi.Status.Phase = virtv2.ImageReady + + err = ds.importerService.Unprotect(ctx, pod) + if err != nil { + return false, err + } + + return CleanUp(ctx, cvi, ds) + case common.IsTerminating(pod): + cvi.Status.Phase = virtv2.ImagePending + + ds.logger.Info("Cleaning up...", "cvi", cvi.Name) + case pod == nil: + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_Provisioning + condition.Message = "DVCR Provisioner not found: create the new one." + + cvi.Status.Phase = virtv2.ImageProvisioning + cvi.Status.Progress = ds.statService.GetProgress(cvi.GetUID(), pod, cvi.Status.Progress) + cvi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(cvi.GetUID(), pod) + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + envSettings := ds.getEnvSettings(cvi, supgen) + err = ds.importerService.Start(ctx, envSettings, cvi, supgen, datasource.NewCABundleForCVMI(cvi.Spec.DataSource)) + if err != nil { + return false, err + } + + ds.logger.Info("Create importer pod...", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", "nil") + case common.IsPodComplete(pod): + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.ReadyReason_Ready + condition.Message = "" + + cvi.Status.Phase = virtv2.ImageReady + cvi.Status.Size = ds.statService.GetSize(pod) + cvi.Status.CDROM = ds.statService.GetCDROM(pod) + cvi.Status.Format = ds.statService.GetFormat(pod) + cvi.Status.Progress = ds.statService.GetProgress(cvi.GetUID(), pod, cvi.Status.Progress) + cvi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(cvi.GetUID(), pod) + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Ready", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + default: + err = ds.statService.CheckPod(pod) + if err != nil { + cvi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_ProvisioningNotStarted + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + case errors.Is(err, service.ErrProvisioningFailed): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.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 = cvicondition.ReadyReason_Provisioning + condition.Message = "Import is in the process of provisioning to DVCR." + + cvi.Status.Phase = virtv2.ImageProvisioning + cvi.Status.Progress = ds.statService.GetProgress(cvi.GetUID(), pod, cvi.Status.Progress) + cvi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(cvi.GetUID(), pod) + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Provisioning...", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + } + + return true, nil +} + +func (ds HTTPDataSource) CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + supgen := supplements.NewGenerator(common.CVIShortName, cvi.Name, ds.controllerNamespace, cvi.UID) + + requeue, err := ds.importerService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + + return requeue, nil +} + +func (ds HTTPDataSource) Validate(_ context.Context, _ *virtv2.ClusterVirtualImage) error { + return nil +} + +func (ds HTTPDataSource) getEnvSettings(cvi *virtv2.ClusterVirtualImage, supgen *supplements.Generator) *importer.Settings { + var settings importer.Settings + + importer.ApplyHTTPSourceSettings(&settings, cvi.Spec.DataSource.HTTP, supgen) + importer.ApplyDVCRDestinationSettings( + &settings, + ds.dvcrSettings, + supgen, + ds.dvcrSettings.RegistryImageForCVMI(cvi.Name), + ) + + return &settings +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/http_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/http_test.go new file mode 100644 index 000000000..915cd2a5a --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/http_test.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 + +// func TestHTTPDataSource_Run(t *testing.T) { +// expectedStatus := getExpectedStatus() +// stat := getStatMock(expectedStatus) +// +// t.Run("to provisioning phase (no importer pod)", func(t *testing.T) { +// var cvi virtv2.ClusterVirtualImage +// cvi.Spec.DataSource.Type = virtv2.DataSourceTypeHTTP +// cvi.Spec.DataSource.HTTP = &virtv2.DataSourceHTTP{ +// URL: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img", +// } +// +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return nil, nil +// }, +// StartFunc: func(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { +// return nil +// }, +// } +// +// ds := NewHTTPDataSource(stat, &impt, &dvcr.Settings{}, "") +// requeue, err := ds.Sync(context.Background(), &cvi) +// require.NoError(t, err) +// require.True(t, requeue) +// +// require.Equal(t, virtv2.ImageProvisioning, cvi.Status.Phase) +// require.Equal(t, expectedStatus.Progress, cvi.Status.Progress) +// require.Equal(t, expectedStatus.DownloadSpeed, cvi.Status.DownloadSpeed) +// require.NotEmpty(t, cvi.Status.Target) +// }) +// +// t.Run("to provisioning phase (with importer pod)", func(t *testing.T) { +// var cvi virtv2.ClusterVirtualImage +// +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{ +// Phase: corev1.PodRunning, +// }, +// }, nil +// }, +// } +// +// ds := NewHTTPDataSource(stat, &impt, &dvcr.Settings{}, "") +// requeue, err := ds.Sync(context.Background(), &cvi) +// require.NoError(t, err) +// require.True(t, requeue) +// +// require.Equal(t, virtv2.ImageProvisioning, cvi.Status.Phase) +// require.Equal(t, expectedStatus.Progress, cvi.Status.Progress) +// require.Equal(t, expectedStatus.DownloadSpeed, cvi.Status.DownloadSpeed) +// require.NotEmpty(t, cvi.Status.Target) +// }) +// +// t.Run("to ready", func(t *testing.T) { +// var cvi virtv2.ClusterVirtualImage +// +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{ +// Phase: corev1.PodSucceeded, +// }, +// }, nil +// }, +// } +// +// ds := NewHTTPDataSource(stat, &impt, &dvcr.Settings{}, "") +// requeue, err := ds.Sync(context.Background(), &cvi) +// require.NoError(t, err) +// require.True(t, requeue) +// +// require.Equal(t, virtv2.ImageReady, cvi.Status.Phase) +// require.Equal(t, expectedStatus.Format, cvi.Status.Format) +// require.Equal(t, expectedStatus.CDROM, cvi.Status.CDROM) +// require.Equal(t, expectedStatus.Size, cvi.Status.Size) +// require.Equal(t, expectedStatus.Progress, cvi.Status.Progress) +// require.Equal(t, expectedStatus.DownloadSpeed, cvi.Status.DownloadSpeed) +// require.NotEmpty(t, cvi.Status.Target) +// }) +// +// t.Run("clean up", func(t *testing.T) { +// var cvi virtv2.ClusterVirtualImage +// cvi.Status = expectedStatus +// cvi.Status.Phase = virtv2.ImageReady +// +// impt := ImporterMock{ +// CleanUpFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// return false, nil +// }, +// } +// +// ds := NewHTTPDataSource(stat, &impt, &dvcr.Settings{}, "") +// requeue, err := ds.Sync(context.Background(), &cvi) +// require.NoError(t, err) +// require.False(t, requeue) +// +// require.Equal(t, virtv2.ImageReady, cvi.Status.Phase) +// require.Equal(t, expectedStatus.Format, cvi.Status.Format) +// require.Equal(t, expectedStatus.CDROM, cvi.Status.CDROM) +// require.Equal(t, expectedStatus.Size, cvi.Status.Size) +// require.Equal(t, expectedStatus.Progress, cvi.Status.Progress) +// require.Equal(t, expectedStatus.DownloadSpeed, cvi.Status.DownloadSpeed) +// require.NotEmpty(t, cvi.Status.Target) +// }) +// } + +// func TestHTTPDataSource_CleanUp(t *testing.T) { +// t.Run("clean up", func(t *testing.T) { +// var cvi virtv2.ClusterVirtualImage +// +// impt := ImporterMock{ +// CleanUpFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// return false, nil +// }, +// } +// +// ds := NewHTTPDataSource(nil, &impt, &dvcr.Settings{}, "") +// requeue, err := ds.CleanUp(context.Background(), &cvi) +// require.NoError(t, err) +// require.False(t, requeue) +// }) +// } + +// func getExpectedStatus() virtv2.ClusterVirtualImageStatus { +// return virtv2.ClusterVirtualImageStatus{ +// ImageStatus: virtv2.ImageStatus{ +// Phase: virtv2.ImagePending, +// DownloadSpeed: virtv2.ImageStatusSpeed{ +// Avg: "000", +// AvgBytes: "111", +// Current: "222", +// CurrentBytes: "333", +// }, +// Size: virtv2.ImageStatusSize{ +// Stored: "AAA", +// StoredBytes: "BBB", +// Unpacked: "CCC", +// UnpackedBytes: "DDD", +// }, +// Format: "qcow2", +// CDROM: true, +// Target: virtv2.ImageStatusTarget{ +// RegistryURL: "dvcr.d8-virtualization.svc/cvi/cvi-example", +// }, +// Progress: "15%", +// }, +// } +// } + +// func getStatMock(expectedStatus virtv2.ClusterVirtualImageStatus) *StatMock { +// return &StatMock{ +// GetCDROMFunc: func(pod *corev1.Pod) bool { +// return expectedStatus.CDROM +// }, +// GetDownloadSpeedFunc: func(ownerUID types.UID, pod *corev1.Pod) virtv2.ImageStatusSpeed { +// return expectedStatus.DownloadSpeed +// }, +// GetFormatFunc: func(pod *corev1.Pod) string { +// return expectedStatus.Format +// }, +// GetProgressFunc: func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { +// return expectedStatus.Progress +// }, +// GetSizeFunc: func(pod *corev1.Pod) virtv2.ImageStatusSize { +// return expectedStatus.Size +// }, +// } +// } diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/interfaces.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/interfaces.go new file mode 100644 index 000000000..17d8f7865 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/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/cvi/internal/source/mock.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/mock.go new file mode 100644 index 000000000..c58e71993 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/mock.go @@ -0,0 +1,1126 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package source + +import ( + "context" + "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" + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/types" + "sync" +) + +// Ensure, that ImporterMock does implement Importer. +// If this is not the case, regenerate this file with moq. +var _ Importer = &ImporterMock{} + +// ImporterMock is a mock implementation of Importer. +// +// func TestSomethingThatUsesImporter(t *testing.T) { +// +// // make and configure a mocked Importer +// mockedImporter := &ImporterMock{ +// CleanUpFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// panic("mock out the GetPod method") +// }, +// ProtectFunc: func(ctx context.Context, pod *corev1.Pod) error { +// panic("mock out the Protect method") +// }, +// StartFunc: func(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { +// panic("mock out the Start method") +// }, +// UnprotectFunc: func(ctx context.Context, pod *corev1.Pod) error { +// panic("mock out the Unprotect method") +// }, +// } +// +// // use mockedImporter in code that requires Importer +// // and then make assertions. +// +// } +type ImporterMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup *supplements.Generator) (bool, error) + + // GetPodFunc mocks the GetPod method. + GetPodFunc func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) + + // ProtectFunc mocks the Protect method. + ProtectFunc func(ctx context.Context, pod *corev1.Pod) error + + // StartFunc mocks the Start method. + StartFunc func(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error + + // UnprotectFunc mocks the Unprotect method. + UnprotectFunc func(ctx context.Context, pod *corev1.Pod) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup *supplements.Generator + } + // GetPod holds details about calls to the GetPod method. + GetPod []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup *supplements.Generator + } + // Protect holds details about calls to the Protect method. + Protect []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Pod is the pod argument value. + Pod *corev1.Pod + } + // Start holds details about calls to the Start method. + Start []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Settings is the settings argument value. + Settings *importer.Settings + // Obj is the obj argument value. + Obj service.ObjectKind + // Sup is the sup argument value. + Sup *supplements.Generator + // CaBundle is the caBundle argument value. + CaBundle *datasource.CABundle + } + // Unprotect holds details about calls to the Unprotect method. + Unprotect []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Pod is the pod argument value. + Pod *corev1.Pod + } + } + lockCleanUp sync.RWMutex + lockGetPod sync.RWMutex + lockProtect sync.RWMutex + lockStart sync.RWMutex + lockUnprotect sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *ImporterMock) CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("ImporterMock.CleanUpFunc: method is nil but Importer.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup *supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedImporter.CleanUpCalls()) +func (mock *ImporterMock) CleanUpCalls() []struct { + Ctx context.Context + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup *supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// GetPod calls GetPodFunc. +func (mock *ImporterMock) GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { + if mock.GetPodFunc == nil { + panic("ImporterMock.GetPodFunc: method is nil but Importer.GetPod was just called") + } + callInfo := struct { + Ctx context.Context + Sup *supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetPod.Lock() + mock.calls.GetPod = append(mock.calls.GetPod, callInfo) + mock.lockGetPod.Unlock() + return mock.GetPodFunc(ctx, sup) +} + +// GetPodCalls gets all the calls that were made to GetPod. +// Check the length with: +// +// len(mockedImporter.GetPodCalls()) +func (mock *ImporterMock) GetPodCalls() []struct { + Ctx context.Context + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup *supplements.Generator + } + mock.lockGetPod.RLock() + calls = mock.calls.GetPod + mock.lockGetPod.RUnlock() + return calls +} + +// Protect calls ProtectFunc. +func (mock *ImporterMock) Protect(ctx context.Context, pod *corev1.Pod) error { + if mock.ProtectFunc == nil { + panic("ImporterMock.ProtectFunc: method is nil but Importer.Protect was just called") + } + callInfo := struct { + Ctx context.Context + Pod *corev1.Pod + }{ + Ctx: ctx, + Pod: pod, + } + mock.lockProtect.Lock() + mock.calls.Protect = append(mock.calls.Protect, callInfo) + mock.lockProtect.Unlock() + return mock.ProtectFunc(ctx, pod) +} + +// ProtectCalls gets all the calls that were made to Protect. +// Check the length with: +// +// len(mockedImporter.ProtectCalls()) +func (mock *ImporterMock) ProtectCalls() []struct { + Ctx context.Context + Pod *corev1.Pod +} { + var calls []struct { + Ctx context.Context + Pod *corev1.Pod + } + mock.lockProtect.RLock() + calls = mock.calls.Protect + mock.lockProtect.RUnlock() + return calls +} + +// Start calls StartFunc. +func (mock *ImporterMock) Start(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { + if mock.StartFunc == nil { + panic("ImporterMock.StartFunc: method is nil but Importer.Start was just called") + } + callInfo := struct { + Ctx context.Context + Settings *importer.Settings + Obj service.ObjectKind + Sup *supplements.Generator + CaBundle *datasource.CABundle + }{ + Ctx: ctx, + Settings: settings, + Obj: obj, + Sup: sup, + CaBundle: caBundle, + } + mock.lockStart.Lock() + mock.calls.Start = append(mock.calls.Start, callInfo) + mock.lockStart.Unlock() + return mock.StartFunc(ctx, settings, obj, sup, caBundle) +} + +// StartCalls gets all the calls that were made to Start. +// Check the length with: +// +// len(mockedImporter.StartCalls()) +func (mock *ImporterMock) StartCalls() []struct { + Ctx context.Context + Settings *importer.Settings + Obj service.ObjectKind + Sup *supplements.Generator + CaBundle *datasource.CABundle +} { + var calls []struct { + Ctx context.Context + Settings *importer.Settings + Obj service.ObjectKind + Sup *supplements.Generator + CaBundle *datasource.CABundle + } + mock.lockStart.RLock() + calls = mock.calls.Start + mock.lockStart.RUnlock() + return calls +} + +// Unprotect calls UnprotectFunc. +func (mock *ImporterMock) Unprotect(ctx context.Context, pod *corev1.Pod) error { + if mock.UnprotectFunc == nil { + panic("ImporterMock.UnprotectFunc: method is nil but Importer.Unprotect was just called") + } + callInfo := struct { + Ctx context.Context + Pod *corev1.Pod + }{ + Ctx: ctx, + Pod: pod, + } + mock.lockUnprotect.Lock() + mock.calls.Unprotect = append(mock.calls.Unprotect, callInfo) + mock.lockUnprotect.Unlock() + return mock.UnprotectFunc(ctx, pod) +} + +// UnprotectCalls gets all the calls that were made to Unprotect. +// Check the length with: +// +// len(mockedImporter.UnprotectCalls()) +func (mock *ImporterMock) UnprotectCalls() []struct { + Ctx context.Context + Pod *corev1.Pod +} { + var calls []struct { + Ctx context.Context + Pod *corev1.Pod + } + mock.lockUnprotect.RLock() + calls = mock.calls.Unprotect + mock.lockUnprotect.RUnlock() + return calls +} + +// Ensure, that UploaderMock does implement Uploader. +// If this is not the case, regenerate this file with moq. +var _ Uploader = &UploaderMock{} + +// UploaderMock is a mock implementation of Uploader. +// +// func TestSomethingThatUsesUploader(t *testing.T) { +// +// // make and configure a mocked Uploader +// mockedUploader := &UploaderMock{ +// CleanUpFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// panic("mock out the CleanUp method") +// }, +// GetIngressFunc: func(ctx context.Context, sup *supplements.Generator) (*netv1.Ingress, error) { +// panic("mock out the GetIngress method") +// }, +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// panic("mock out the GetPod method") +// }, +// GetServiceFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Service, error) { +// panic("mock out the GetService method") +// }, +// ProtectFunc: func(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { +// panic("mock out the Protect method") +// }, +// StartFunc: func(ctx context.Context, settings *uploader.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { +// panic("mock out the Start method") +// }, +// UnprotectFunc: func(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { +// panic("mock out the Unprotect method") +// }, +// } +// +// // use mockedUploader in code that requires Uploader +// // and then make assertions. +// +// } +type UploaderMock struct { + // CleanUpFunc mocks the CleanUp method. + CleanUpFunc func(ctx context.Context, sup *supplements.Generator) (bool, error) + + // GetIngressFunc mocks the GetIngress method. + GetIngressFunc func(ctx context.Context, sup *supplements.Generator) (*netv1.Ingress, error) + + // GetPodFunc mocks the GetPod method. + GetPodFunc func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) + + // GetServiceFunc mocks the GetService method. + GetServiceFunc func(ctx context.Context, sup *supplements.Generator) (*corev1.Service, error) + + // ProtectFunc mocks the Protect method. + ProtectFunc func(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error + + // StartFunc mocks the Start method. + StartFunc func(ctx context.Context, settings *uploader.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error + + // UnprotectFunc mocks the Unprotect method. + UnprotectFunc func(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error + + // calls tracks calls to the methods. + calls struct { + // CleanUp holds details about calls to the CleanUp method. + CleanUp []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup *supplements.Generator + } + // GetIngress holds details about calls to the GetIngress method. + GetIngress []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup *supplements.Generator + } + // GetPod holds details about calls to the GetPod method. + GetPod []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup *supplements.Generator + } + // GetService holds details about calls to the GetService method. + GetService []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Sup is the sup argument value. + Sup *supplements.Generator + } + // Protect holds details about calls to the Protect method. + Protect []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Pod is the pod argument value. + Pod *corev1.Pod + // Svc is the svc argument value. + Svc *corev1.Service + // Ing is the ing argument value. + Ing *netv1.Ingress + } + // Start holds details about calls to the Start method. + Start []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Settings is the settings argument value. + Settings *uploader.Settings + // Obj is the obj argument value. + Obj service.ObjectKind + // Sup is the sup argument value. + Sup *supplements.Generator + // CaBundle is the caBundle argument value. + CaBundle *datasource.CABundle + } + // Unprotect holds details about calls to the Unprotect method. + Unprotect []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Pod is the pod argument value. + Pod *corev1.Pod + // Svc is the svc argument value. + Svc *corev1.Service + // Ing is the ing argument value. + Ing *netv1.Ingress + } + } + lockCleanUp sync.RWMutex + lockGetIngress sync.RWMutex + lockGetPod sync.RWMutex + lockGetService sync.RWMutex + lockProtect sync.RWMutex + lockStart sync.RWMutex + lockUnprotect sync.RWMutex +} + +// CleanUp calls CleanUpFunc. +func (mock *UploaderMock) CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) { + if mock.CleanUpFunc == nil { + panic("UploaderMock.CleanUpFunc: method is nil but Uploader.CleanUp was just called") + } + callInfo := struct { + Ctx context.Context + Sup *supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockCleanUp.Lock() + mock.calls.CleanUp = append(mock.calls.CleanUp, callInfo) + mock.lockCleanUp.Unlock() + return mock.CleanUpFunc(ctx, sup) +} + +// CleanUpCalls gets all the calls that were made to CleanUp. +// Check the length with: +// +// len(mockedUploader.CleanUpCalls()) +func (mock *UploaderMock) CleanUpCalls() []struct { + Ctx context.Context + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup *supplements.Generator + } + mock.lockCleanUp.RLock() + calls = mock.calls.CleanUp + mock.lockCleanUp.RUnlock() + return calls +} + +// GetIngress calls GetIngressFunc. +func (mock *UploaderMock) GetIngress(ctx context.Context, sup *supplements.Generator) (*netv1.Ingress, error) { + if mock.GetIngressFunc == nil { + panic("UploaderMock.GetIngressFunc: method is nil but Uploader.GetIngress was just called") + } + callInfo := struct { + Ctx context.Context + Sup *supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetIngress.Lock() + mock.calls.GetIngress = append(mock.calls.GetIngress, callInfo) + mock.lockGetIngress.Unlock() + return mock.GetIngressFunc(ctx, sup) +} + +// GetIngressCalls gets all the calls that were made to GetIngress. +// Check the length with: +// +// len(mockedUploader.GetIngressCalls()) +func (mock *UploaderMock) GetIngressCalls() []struct { + Ctx context.Context + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup *supplements.Generator + } + mock.lockGetIngress.RLock() + calls = mock.calls.GetIngress + mock.lockGetIngress.RUnlock() + return calls +} + +// GetPod calls GetPodFunc. +func (mock *UploaderMock) GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { + if mock.GetPodFunc == nil { + panic("UploaderMock.GetPodFunc: method is nil but Uploader.GetPod was just called") + } + callInfo := struct { + Ctx context.Context + Sup *supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetPod.Lock() + mock.calls.GetPod = append(mock.calls.GetPod, callInfo) + mock.lockGetPod.Unlock() + return mock.GetPodFunc(ctx, sup) +} + +// GetPodCalls gets all the calls that were made to GetPod. +// Check the length with: +// +// len(mockedUploader.GetPodCalls()) +func (mock *UploaderMock) GetPodCalls() []struct { + Ctx context.Context + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup *supplements.Generator + } + mock.lockGetPod.RLock() + calls = mock.calls.GetPod + mock.lockGetPod.RUnlock() + return calls +} + +// GetService calls GetServiceFunc. +func (mock *UploaderMock) GetService(ctx context.Context, sup *supplements.Generator) (*corev1.Service, error) { + if mock.GetServiceFunc == nil { + panic("UploaderMock.GetServiceFunc: method is nil but Uploader.GetService was just called") + } + callInfo := struct { + Ctx context.Context + Sup *supplements.Generator + }{ + Ctx: ctx, + Sup: sup, + } + mock.lockGetService.Lock() + mock.calls.GetService = append(mock.calls.GetService, callInfo) + mock.lockGetService.Unlock() + return mock.GetServiceFunc(ctx, sup) +} + +// GetServiceCalls gets all the calls that were made to GetService. +// Check the length with: +// +// len(mockedUploader.GetServiceCalls()) +func (mock *UploaderMock) GetServiceCalls() []struct { + Ctx context.Context + Sup *supplements.Generator +} { + var calls []struct { + Ctx context.Context + Sup *supplements.Generator + } + mock.lockGetService.RLock() + calls = mock.calls.GetService + mock.lockGetService.RUnlock() + return calls +} + +// Protect calls ProtectFunc. +func (mock *UploaderMock) Protect(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { + if mock.ProtectFunc == nil { + panic("UploaderMock.ProtectFunc: method is nil but Uploader.Protect was just called") + } + callInfo := struct { + Ctx context.Context + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + }{ + Ctx: ctx, + Pod: pod, + Svc: svc, + Ing: ing, + } + mock.lockProtect.Lock() + mock.calls.Protect = append(mock.calls.Protect, callInfo) + mock.lockProtect.Unlock() + return mock.ProtectFunc(ctx, pod, svc, ing) +} + +// ProtectCalls gets all the calls that were made to Protect. +// Check the length with: +// +// len(mockedUploader.ProtectCalls()) +func (mock *UploaderMock) ProtectCalls() []struct { + Ctx context.Context + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress +} { + var calls []struct { + Ctx context.Context + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + } + mock.lockProtect.RLock() + calls = mock.calls.Protect + mock.lockProtect.RUnlock() + return calls +} + +// Start calls StartFunc. +func (mock *UploaderMock) Start(ctx context.Context, settings *uploader.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { + if mock.StartFunc == nil { + panic("UploaderMock.StartFunc: method is nil but Uploader.Start was just called") + } + callInfo := struct { + Ctx context.Context + Settings *uploader.Settings + Obj service.ObjectKind + Sup *supplements.Generator + CaBundle *datasource.CABundle + }{ + Ctx: ctx, + Settings: settings, + Obj: obj, + Sup: sup, + CaBundle: caBundle, + } + mock.lockStart.Lock() + mock.calls.Start = append(mock.calls.Start, callInfo) + mock.lockStart.Unlock() + return mock.StartFunc(ctx, settings, obj, sup, caBundle) +} + +// StartCalls gets all the calls that were made to Start. +// Check the length with: +// +// len(mockedUploader.StartCalls()) +func (mock *UploaderMock) StartCalls() []struct { + Ctx context.Context + Settings *uploader.Settings + Obj service.ObjectKind + Sup *supplements.Generator + CaBundle *datasource.CABundle +} { + var calls []struct { + Ctx context.Context + Settings *uploader.Settings + Obj service.ObjectKind + Sup *supplements.Generator + CaBundle *datasource.CABundle + } + mock.lockStart.RLock() + calls = mock.calls.Start + mock.lockStart.RUnlock() + return calls +} + +// Unprotect calls UnprotectFunc. +func (mock *UploaderMock) Unprotect(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { + if mock.UnprotectFunc == nil { + panic("UploaderMock.UnprotectFunc: method is nil but Uploader.Unprotect was just called") + } + callInfo := struct { + Ctx context.Context + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + }{ + Ctx: ctx, + Pod: pod, + Svc: svc, + Ing: ing, + } + mock.lockUnprotect.Lock() + mock.calls.Unprotect = append(mock.calls.Unprotect, callInfo) + mock.lockUnprotect.Unlock() + return mock.UnprotectFunc(ctx, pod, svc, ing) +} + +// UnprotectCalls gets all the calls that were made to Unprotect. +// Check the length with: +// +// len(mockedUploader.UnprotectCalls()) +func (mock *UploaderMock) UnprotectCalls() []struct { + Ctx context.Context + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress +} { + var calls []struct { + Ctx context.Context + Pod *corev1.Pod + Svc *corev1.Service + Ing *netv1.Ingress + } + mock.lockUnprotect.RLock() + calls = mock.calls.Unprotect + mock.lockUnprotect.RUnlock() + return calls +} + +// Ensure, that StatMock does implement Stat. +// If this is not the case, regenerate this file with moq. +var _ Stat = &StatMock{} + +// StatMock is a mock implementation of Stat. +// +// func TestSomethingThatUsesStat(t *testing.T) { +// +// // make and configure a mocked Stat +// mockedStat := &StatMock{ +// CheckPodFunc: func(pod *corev1.Pod) error { +// panic("mock out the CheckPod method") +// }, +// GetCDROMFunc: func(pod *corev1.Pod) bool { +// panic("mock out the GetCDROM method") +// }, +// GetDownloadSpeedFunc: func(ownerUID types.UID, pod *corev1.Pod) virtv2.ImageStatusSpeed { +// panic("mock out the GetDownloadSpeed method") +// }, +// GetFormatFunc: func(pod *corev1.Pod) string { +// panic("mock out the GetFormat method") +// }, +// GetProgressFunc: func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { +// panic("mock out the GetProgress method") +// }, +// GetReasonErrorFunc: func(pod *corev1.Pod) (string, string, error) { +// panic("mock out the GetReasonError method") +// }, +// GetSizeFunc: func(pod *corev1.Pod) virtv2.ImageStatusSize { +// panic("mock out the GetSize method") +// }, +// IsUploadStartedFunc: func(ownerUID types.UID, pod *corev1.Pod) bool { +// panic("mock out the IsUploadStarted method") +// }, +// } +// +// // use mockedStat in code that requires Stat +// // and then make assertions. +// +// } +type StatMock struct { + // CheckPodFunc mocks the CheckPod method. + CheckPodFunc func(pod *corev1.Pod) error + + // GetCDROMFunc mocks the GetCDROM method. + GetCDROMFunc func(pod *corev1.Pod) bool + + // GetDownloadSpeedFunc mocks the GetDownloadSpeed method. + GetDownloadSpeedFunc func(ownerUID types.UID, pod *corev1.Pod) virtv2.ImageStatusSpeed + + // GetFormatFunc mocks the GetFormat method. + GetFormatFunc func(pod *corev1.Pod) string + + // GetProgressFunc mocks the GetProgress method. + GetProgressFunc func(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string + + // GetReasonErrorFunc mocks the GetReasonError method. + GetReasonErrorFunc func(pod *corev1.Pod) (string, string, error) + + // GetSizeFunc mocks the GetSize method. + GetSizeFunc func(pod *corev1.Pod) virtv2.ImageStatusSize + + // IsUploadStartedFunc mocks the IsUploadStarted method. + IsUploadStartedFunc func(ownerUID types.UID, pod *corev1.Pod) bool + + // calls tracks calls to the methods. + calls struct { + // CheckPod holds details about calls to the CheckPod method. + CheckPod []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetCDROM holds details about calls to the GetCDROM method. + GetCDROM []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetDownloadSpeed holds details about calls to the GetDownloadSpeed method. + GetDownloadSpeed []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetFormat holds details about calls to the GetFormat method. + GetFormat []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetProgress holds details about calls to the GetProgress method. + GetProgress []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + // PrevProgress is the prevProgress argument value. + PrevProgress string + // Opts is the opts argument value. + Opts []service.GetProgressOption + } + // GetReasonError holds details about calls to the GetReasonError method. + GetReasonError []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // GetSize holds details about calls to the GetSize method. + GetSize []struct { + // Pod is the pod argument value. + Pod *corev1.Pod + } + // IsUploadStarted holds details about calls to the IsUploadStarted method. + IsUploadStarted []struct { + // OwnerUID is the ownerUID argument value. + OwnerUID types.UID + // Pod is the pod argument value. + Pod *corev1.Pod + } + } + lockCheckPod sync.RWMutex + lockGetCDROM sync.RWMutex + lockGetDownloadSpeed sync.RWMutex + lockGetFormat sync.RWMutex + lockGetProgress sync.RWMutex + lockGetReasonError sync.RWMutex + lockGetSize sync.RWMutex + lockIsUploadStarted sync.RWMutex +} + +// CheckPod calls CheckPodFunc. +func (mock *StatMock) CheckPod(pod *corev1.Pod) error { + if mock.CheckPodFunc == nil { + panic("StatMock.CheckPodFunc: method is nil but Stat.CheckPod was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockCheckPod.Lock() + mock.calls.CheckPod = append(mock.calls.CheckPod, callInfo) + mock.lockCheckPod.Unlock() + return mock.CheckPodFunc(pod) +} + +// CheckPodCalls gets all the calls that were made to CheckPod. +// Check the length with: +// +// len(mockedStat.CheckPodCalls()) +func (mock *StatMock) CheckPodCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockCheckPod.RLock() + calls = mock.calls.CheckPod + mock.lockCheckPod.RUnlock() + return calls +} + +// GetCDROM calls GetCDROMFunc. +func (mock *StatMock) GetCDROM(pod *corev1.Pod) bool { + if mock.GetCDROMFunc == nil { + panic("StatMock.GetCDROMFunc: method is nil but Stat.GetCDROM was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetCDROM.Lock() + mock.calls.GetCDROM = append(mock.calls.GetCDROM, callInfo) + mock.lockGetCDROM.Unlock() + return mock.GetCDROMFunc(pod) +} + +// GetCDROMCalls gets all the calls that were made to GetCDROM. +// Check the length with: +// +// len(mockedStat.GetCDROMCalls()) +func (mock *StatMock) GetCDROMCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetCDROM.RLock() + calls = mock.calls.GetCDROM + mock.lockGetCDROM.RUnlock() + return calls +} + +// GetDownloadSpeed calls GetDownloadSpeedFunc. +func (mock *StatMock) GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) virtv2.ImageStatusSpeed { + if mock.GetDownloadSpeedFunc == nil { + panic("StatMock.GetDownloadSpeedFunc: method is nil but Stat.GetDownloadSpeed was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + }{ + OwnerUID: ownerUID, + Pod: pod, + } + mock.lockGetDownloadSpeed.Lock() + mock.calls.GetDownloadSpeed = append(mock.calls.GetDownloadSpeed, callInfo) + mock.lockGetDownloadSpeed.Unlock() + return mock.GetDownloadSpeedFunc(ownerUID, pod) +} + +// GetDownloadSpeedCalls gets all the calls that were made to GetDownloadSpeed. +// Check the length with: +// +// len(mockedStat.GetDownloadSpeedCalls()) +func (mock *StatMock) GetDownloadSpeedCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + } + mock.lockGetDownloadSpeed.RLock() + calls = mock.calls.GetDownloadSpeed + mock.lockGetDownloadSpeed.RUnlock() + return calls +} + +// GetFormat calls GetFormatFunc. +func (mock *StatMock) GetFormat(pod *corev1.Pod) string { + if mock.GetFormatFunc == nil { + panic("StatMock.GetFormatFunc: method is nil but Stat.GetFormat was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetFormat.Lock() + mock.calls.GetFormat = append(mock.calls.GetFormat, callInfo) + mock.lockGetFormat.Unlock() + return mock.GetFormatFunc(pod) +} + +// GetFormatCalls gets all the calls that were made to GetFormat. +// Check the length with: +// +// len(mockedStat.GetFormatCalls()) +func (mock *StatMock) GetFormatCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetFormat.RLock() + calls = mock.calls.GetFormat + mock.lockGetFormat.RUnlock() + return calls +} + +// GetProgress calls GetProgressFunc. +func (mock *StatMock) GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...service.GetProgressOption) string { + if mock.GetProgressFunc == nil { + panic("StatMock.GetProgressFunc: method is nil but Stat.GetProgress was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption + }{ + OwnerUID: ownerUID, + Pod: pod, + PrevProgress: prevProgress, + Opts: opts, + } + mock.lockGetProgress.Lock() + mock.calls.GetProgress = append(mock.calls.GetProgress, callInfo) + mock.lockGetProgress.Unlock() + return mock.GetProgressFunc(ownerUID, pod, prevProgress, opts...) +} + +// GetProgressCalls gets all the calls that were made to GetProgress. +// Check the length with: +// +// len(mockedStat.GetProgressCalls()) +func (mock *StatMock) GetProgressCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + PrevProgress string + Opts []service.GetProgressOption + } + mock.lockGetProgress.RLock() + calls = mock.calls.GetProgress + mock.lockGetProgress.RUnlock() + return calls +} + +// GetReasonError calls GetReasonErrorFunc. +func (mock *StatMock) GetReasonError(pod *corev1.Pod) (string, string, error) { + if mock.GetReasonErrorFunc == nil { + panic("StatMock.GetReasonErrorFunc: method is nil but Stat.GetReasonError was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetReasonError.Lock() + mock.calls.GetReasonError = append(mock.calls.GetReasonError, callInfo) + mock.lockGetReasonError.Unlock() + return mock.GetReasonErrorFunc(pod) +} + +// GetReasonErrorCalls gets all the calls that were made to GetReasonError. +// Check the length with: +// +// len(mockedStat.GetReasonErrorCalls()) +func (mock *StatMock) GetReasonErrorCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetReasonError.RLock() + calls = mock.calls.GetReasonError + mock.lockGetReasonError.RUnlock() + return calls +} + +// GetSize calls GetSizeFunc. +func (mock *StatMock) GetSize(pod *corev1.Pod) virtv2.ImageStatusSize { + if mock.GetSizeFunc == nil { + panic("StatMock.GetSizeFunc: method is nil but Stat.GetSize was just called") + } + callInfo := struct { + Pod *corev1.Pod + }{ + Pod: pod, + } + mock.lockGetSize.Lock() + mock.calls.GetSize = append(mock.calls.GetSize, callInfo) + mock.lockGetSize.Unlock() + return mock.GetSizeFunc(pod) +} + +// GetSizeCalls gets all the calls that were made to GetSize. +// Check the length with: +// +// len(mockedStat.GetSizeCalls()) +func (mock *StatMock) GetSizeCalls() []struct { + Pod *corev1.Pod +} { + var calls []struct { + Pod *corev1.Pod + } + mock.lockGetSize.RLock() + calls = mock.calls.GetSize + mock.lockGetSize.RUnlock() + return calls +} + +// IsUploadStarted calls IsUploadStartedFunc. +func (mock *StatMock) IsUploadStarted(ownerUID types.UID, pod *corev1.Pod) bool { + if mock.IsUploadStartedFunc == nil { + panic("StatMock.IsUploadStartedFunc: method is nil but Stat.IsUploadStarted was just called") + } + callInfo := struct { + OwnerUID types.UID + Pod *corev1.Pod + }{ + OwnerUID: ownerUID, + Pod: pod, + } + mock.lockIsUploadStarted.Lock() + mock.calls.IsUploadStarted = append(mock.calls.IsUploadStarted, callInfo) + mock.lockIsUploadStarted.Unlock() + return mock.IsUploadStartedFunc(ownerUID, pod) +} + +// IsUploadStartedCalls gets all the calls that were made to IsUploadStarted. +// Check the length with: +// +// len(mockedStat.IsUploadStartedCalls()) +func (mock *StatMock) IsUploadStartedCalls() []struct { + OwnerUID types.UID + Pod *corev1.Pod +} { + var calls []struct { + OwnerUID types.UID + Pod *corev1.Pod + } + mock.lockIsUploadStarted.RLock() + calls = mock.calls.IsUploadStarted + mock.lockIsUploadStarted.RUnlock() + return calls +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref.go new file mode 100644 index 000000000..fa2e86d74 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref.go @@ -0,0 +1,225 @@ +/* +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" + "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/dvcr" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type ObjectRefDataSource struct { + statService Stat + importerService Importer + dvcrSettings *dvcr.Settings + client client.Client + controllerNamespace string + logger *slog.Logger +} + +func NewObjectRefDataSource( + statService Stat, + importerService Importer, + dvcrSettings *dvcr.Settings, + client client.Client, + controllerNamespace string, +) *ObjectRefDataSource { + return &ObjectRefDataSource{ + statService: statService, + importerService: importerService, + dvcrSettings: dvcrSettings, + client: client, + controllerNamespace: controllerNamespace, + logger: slog.Default().With("controller", common.CVIShortName, "ds", "objectref"), + } +} + +func (ds ObjectRefDataSource) Sync(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + ds.logger.Info("Sync", "cvi", cvi.Name) + + condition, _ := service.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + defer func() { service.SetCondition(condition, &cvi.Status.Conditions) }() + + supgen := supplements.NewGenerator(common.CVIShortName, cvi.Name, ds.controllerNamespace, cvi.UID) + pod, err := ds.importerService.GetPod(ctx, supgen) + if err != nil { + return false, err + } + + switch { + case isDiskProvisioningFinished(condition): + ds.logger.Info("Finishing...", "cvi", cvi.Name) + + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.ReadyReason_Ready + condition.Message = "" + + cvi.Status.Phase = virtv2.ImageReady + + err = ds.importerService.Unprotect(ctx, pod) + if err != nil { + return false, err + } + + return CleanUp(ctx, cvi, ds) + case common.IsTerminating(pod): + + cvi.Status.Phase = virtv2.ImagePending + + ds.logger.Info("Cleaning up...", "cvi", cvi.Name) + case pod == nil: + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_Provisioning + condition.Message = "DVCR Provisioner not found: create the new one." + + var envSettings *importer.Settings + envSettings, err = ds.getEnvSettings(cvi, supgen) + if err != nil { + return false, err + } + + err = ds.importerService.Start(ctx, envSettings, cvi, supgen, datasource.NewCABundleForCVMI(cvi.Spec.DataSource)) + if err != nil { + return false, err + } + + cvi.Status.Phase = virtv2.ImageProvisioning + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Ready", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", "nil") + case common.IsPodComplete(pod): + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.ReadyReason_Ready + condition.Message = "" + + cvi.Status.Phase = virtv2.ImageReady + cvi.Status.Size = ds.statService.GetSize(pod) + cvi.Status.CDROM = ds.statService.GetCDROM(pod) + cvi.Status.Format = ds.statService.GetFormat(pod) + cvi.Status.Progress = "100%" + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Ready", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + default: + err = ds.statService.CheckPod(pod) + if err != nil { + cvi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_ProvisioningNotStarted + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + case errors.Is(err, service.ErrProvisioningFailed): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_ProvisioningFailed + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + default: + return false, err + } + } + + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_Provisioning + condition.Message = "Import is in the process of provisioning to DVCR." + + cvi.Status.Phase = virtv2.ImageProvisioning + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Ready", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + } + + return true, nil +} + +func (ds ObjectRefDataSource) CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + supgen := supplements.NewGenerator(common.CVIShortName, cvi.Name, ds.controllerNamespace, cvi.UID) + + requeue, err := ds.importerService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + + return requeue, nil +} + +func (ds ObjectRefDataSource) Validate(ctx context.Context, cvi *virtv2.ClusterVirtualImage) error { + if cvi.Spec.DataSource.ObjectRef == nil { + return fmt.Errorf("nil object ref: %s", cvi.Spec.DataSource.Type) + } + + dvcrDataSource, err := controller.NewDVCRDataSourcesForCVMI(ctx, cvi.Spec.DataSource, ds.client) + if err != nil { + return err + } + + if dvcrDataSource.IsReady() { + return nil + } + + switch cvi.Spec.DataSource.ObjectRef.Kind { + case virtv2.ClusterVirtualImageObjectRefKindVirtualImage: + return NewImageNotReadyError(cvi.Spec.DataSource.ObjectRef.Name) + case virtv2.ClusterVirtualImageObjectRefKindClusterVirtualImage: + return NewClusterImageNotReadyError(cvi.Spec.DataSource.ObjectRef.Name) + default: + return fmt.Errorf("unexpected object ref kind: %s", cvi.Spec.DataSource.ObjectRef.Kind) + } +} + +func (ds ObjectRefDataSource) getEnvSettings(cvi *virtv2.ClusterVirtualImage, supgen *supplements.Generator) (*importer.Settings, error) { + var settings importer.Settings + + switch cvi.Spec.DataSource.ObjectRef.Kind { + case virtv2.ClusterVirtualImageObjectRefKindVirtualImage: + dvcrSourceImageName := ds.dvcrSettings.RegistryImageForVMI( + cvi.Spec.DataSource.ObjectRef.Name, + cvi.Spec.DataSource.ObjectRef.Namespace, + ) + importer.ApplyDVCRSourceSettings(&settings, dvcrSourceImageName) + case virtv2.ClusterVirtualImageObjectRefKindClusterVirtualImage: + dvcrSourceImageName := ds.dvcrSettings.RegistryImageForCVMI(cvi.Spec.DataSource.ObjectRef.Name) + importer.ApplyDVCRSourceSettings(&settings, dvcrSourceImageName) + default: + return nil, fmt.Errorf("unknown objectRef kind: %s", cvi.Spec.DataSource.ObjectRef.Kind) + } + + importer.ApplyDVCRDestinationSettings( + &settings, + ds.dvcrSettings, + supgen, + ds.dvcrSettings.RegistryImageForCVMI(cvi.Name), + ) + + return &settings, nil +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref_test.go new file mode 100644 index 000000000..40ecb2f00 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/object_ref_test.go @@ -0,0 +1,110 @@ +/* +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 + +// var _ = Describe("ObjectRefDataSource Run", func() { +// expectedStatus := getExpectedStatus() +// stat := getStatMock(expectedStatus) +// +// var cvi *virtv2.ClusterVirtualImage +// +// BeforeEach(func() { +// cvi = &virtv2.ClusterVirtualImage{} +// cvi.Spec.DataSource.Type = virtv2.DataSourceTypeObjectRef +// cvi.Spec.DataSource.ObjectRef = &virtv2.ClusterVirtualImageObjectRef{ +// Kind: virtv2.ClusterVirtualImageObjectRefKindClusterVirtualImage, +// } +// }) +// +// It("To provisioning phase (no importer pod)", func() { +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return nil, nil +// }, +// StartFunc: func(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { +// return nil +// }, +// } +// +// ds := NewObjectRefDataSource(stat, &impt, &dvcr.Settings{}, nil, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("To provisioning phase (with importer pod)", func() { +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{ +// Phase: corev1.PodRunning, +// }, +// }, nil +// }, +// } +// +// ds := NewObjectRefDataSource(stat, &impt, &dvcr.Settings{}, nil, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("To ready phase", func() { +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{ +// Phase: corev1.PodSucceeded, +// }, +// }, nil +// }, +// } +// +// ds := NewObjectRefDataSource(stat, &impt, &dvcr.Settings{}, nil, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageReady)) +// Expect(cvi.Status.Size).To(Equal(expectedStatus.Size)) +// Expect(cvi.Status.CDROM).To(Equal(expectedStatus.CDROM)) +// Expect(cvi.Status.Format).To(Equal(expectedStatus.Format)) +// Expect(cvi.Status.Progress).To(Equal("100%")) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("Clean up", func() { +// cvi.Status.Phase = virtv2.ImageReady +// impt := ImporterMock{ +// CleanUpFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// return true, nil +// }, +// } +// +// ds := NewObjectRefDataSource(stat, &impt, &dvcr.Settings{}, nil, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// }) +// }) diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/registry.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/registry.go new file mode 100644 index 000000000..6cda67fd9 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/registry.go @@ -0,0 +1,205 @@ +/* +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/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/cvicondition" +) + +type RegistryDataSource struct { + statService Stat + importerService Importer + dvcrSettings *dvcr.Settings + client client.Client + controllerNamespace string + logger *slog.Logger +} + +func NewRegistryDataSource( + statService Stat, + importerService Importer, + dvcrSettings *dvcr.Settings, + client client.Client, + controllerNamespace string, +) *RegistryDataSource { + return &RegistryDataSource{ + statService: statService, + importerService: importerService, + dvcrSettings: dvcrSettings, + client: client, + controllerNamespace: controllerNamespace, + logger: slog.Default().With("controller", common.CVIShortName, "ds", "registry"), + } +} + +func (ds RegistryDataSource) Sync(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + ds.logger.Info("Sync", "cvi", cvi.Name) + + condition, _ := service.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + defer func() { service.SetCondition(condition, &cvi.Status.Conditions) }() + + supgen := supplements.NewGenerator(common.CVIShortName, cvi.Name, ds.controllerNamespace, cvi.UID) + pod, err := ds.importerService.GetPod(ctx, supgen) + if err != nil { + return false, err + } + + switch { + case isDiskProvisioningFinished(condition): + ds.logger.Info("Finishing...", "cvi", cvi.Name) + + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.ReadyReason_Ready + condition.Message = "" + + cvi.Status.Phase = virtv2.ImageReady + + err = ds.importerService.Unprotect(ctx, pod) + if err != nil { + return false, err + } + + return CleanUp(ctx, cvi, ds) + case common.IsTerminating(pod): + cvi.Status.Phase = virtv2.ImagePending + + ds.logger.Info("Cleaning up...", "cvi", cvi.Name) + case pod == nil: + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_Provisioning + condition.Message = "DVCR Provisioner not found: create the new one." + + envSettings := ds.getEnvSettings(cvi, supgen) + err = ds.importerService.Start(ctx, envSettings, cvi, supgen, datasource.NewCABundleForCVMI(cvi.Spec.DataSource)) + if err != nil { + return false, err + } + + cvi.Status.Phase = virtv2.ImageProvisioning + cvi.Status.Progress = "0%" + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Create importer pod...", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", "nil") + case common.IsPodComplete(pod): + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.ReadyReason_Ready + condition.Message = "" + + cvi.Status.Phase = virtv2.ImageReady + cvi.Status.Size = ds.statService.GetSize(pod) + cvi.Status.CDROM = ds.statService.GetCDROM(pod) + cvi.Status.Format = ds.statService.GetFormat(pod) + cvi.Status.Progress = "100%" + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Ready", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + default: + err = ds.statService.CheckPod(pod) + if err != nil { + cvi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_ProvisioningNotStarted + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + case errors.Is(err, service.ErrProvisioningFailed): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_ProvisioningFailed + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + default: + return false, err + } + } + + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_Provisioning + condition.Message = "Import is in the process of provisioning to DVCR." + + cvi.Status.Phase = virtv2.ImageProvisioning + cvi.Status.Progress = "0%" + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Provisioning...", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + } + + return true, nil +} + +func (ds RegistryDataSource) CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + supgen := supplements.NewGenerator(common.CVIShortName, cvi.Name, ds.controllerNamespace, cvi.UID) + + requeue, err := ds.importerService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + + return requeue, nil +} + +func (ds RegistryDataSource) Validate(ctx context.Context, cvi *virtv2.ClusterVirtualImage) error { + if cvi.Spec.DataSource.ContainerImage.ImagePullSecret.Name != "" { + secretName := types.NamespacedName{ + Namespace: cvi.Spec.DataSource.ContainerImage.ImagePullSecret.Namespace, + Name: cvi.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(cvi *virtv2.ClusterVirtualImage, supgen *supplements.Generator) *importer.Settings { + var settings importer.Settings + + importer.ApplyRegistrySourceSettings(&settings, cvi.Spec.DataSource.ContainerImage, supgen) + importer.ApplyDVCRDestinationSettings( + &settings, + ds.dvcrSettings, + supgen, + ds.dvcrSettings.RegistryImageForCVMI(cvi.Name), + ) + + return &settings +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/registry_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/registry_test.go new file mode 100644 index 000000000..70204b407 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/registry_test.go @@ -0,0 +1,112 @@ +/* +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 + +// var _ = Describe("RegistryDataSource Run", func() { +// expectedStatus := getExpectedStatus() +// stat := getStatMock(expectedStatus) +// +// var cvi *virtv2.ClusterVirtualImage +// +// BeforeEach(func() { +// cvi = &virtv2.ClusterVirtualImage{} +// cvi.Spec.DataSource.Type = virtv2.DataSourceTypeContainerImage +// cvi.Spec.DataSource.ContainerImage = &virtv2.DataSourceContainerRegistry{ +// Image: "registry.hub.example.com/example/example:latest", +// } +// }) +// +// It("To provisioning phase (no importer pod)", func() { +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return nil, nil +// }, +// StartFunc: func(ctx context.Context, settings *importer.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { +// return nil +// }, +// } +// +// ds := NewRegistryDataSource(stat, &impt, &dvcr.Settings{}, nil, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) +// Expect(cvi.Status.Progress).To(Equal("0%")) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("To provisioning phase (with importer pod)", func() { +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{ +// Phase: corev1.PodRunning, +// }, +// }, nil +// }, +// } +// +// ds := NewRegistryDataSource(stat, &impt, &dvcr.Settings{}, nil, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) +// Expect(cvi.Status.Progress).To(Equal("0%")) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("To ready phase", func() { +// impt := ImporterMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{ +// Phase: corev1.PodSucceeded, +// }, +// }, nil +// }, +// } +// +// ds := NewRegistryDataSource(stat, &impt, &dvcr.Settings{}, nil, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageReady)) +// Expect(cvi.Status.Size).To(Equal(expectedStatus.Size)) +// Expect(cvi.Status.CDROM).To(Equal(expectedStatus.CDROM)) +// Expect(cvi.Status.Format).To(Equal(expectedStatus.Format)) +// Expect(cvi.Status.Progress).To(Equal("100%")) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("Clean up", func() { +// cvi.Status.Phase = virtv2.ImageReady +// impt := ImporterMock{ +// CleanUpFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// return true, nil +// }, +// } +// +// ds := NewRegistryDataSource(stat, &impt, &dvcr.Settings{}, nil, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// }) +// }) diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/sources.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/sources.go new file mode 100644 index 000000000..23c2449ea --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/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/cvicondition" +) + +type Handler interface { + Sync(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) + CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) + Validate(ctx context.Context, cvi *virtv2.ClusterVirtualImage) 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) Get(dsType virtv2.DataSourceType) (Handler, bool) { + source, ok := s.sources[dsType] + return source, ok +} + +func (s Sources) Changed(_ context.Context, cvi *virtv2.ClusterVirtualImage) bool { + return cvi.Generation != cvi.Status.ObservedGeneration +} + +func (s Sources) CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + var requeue bool + + for _, source := range s.sources { + ok, err := source.CleanUp(ctx, cvi) + if err != nil { + return false, err + } + + requeue = requeue || ok + } + + return requeue, nil +} + +type Cleaner interface { + CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) +} + +func CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage, c Cleaner) (bool, error) { + if cc.ShouldCleanupSubResources(cvi) { + return c.CleanUp(ctx, cvi) + } + + return false, nil +} + +func isDiskProvisioningFinished(c metav1.Condition) bool { + return c.Reason == cvicondition.ReadyReason_Ready +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go new file mode 100644 index 000000000..faaaa3ef0 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go @@ -0,0 +1,211 @@ +/* +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/dvcr" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type UploadDataSource struct { + statService Stat + uploaderService Uploader + dvcrSettings *dvcr.Settings + controllerNamespace string + logger *slog.Logger +} + +func NewUploadDataSource( + statService Stat, + uploaderService Uploader, + dvcrSettings *dvcr.Settings, + controllerNamespace string, +) *UploadDataSource { + return &UploadDataSource{ + statService: statService, + uploaderService: uploaderService, + dvcrSettings: dvcrSettings, + controllerNamespace: controllerNamespace, + logger: slog.Default().With("controller", common.CVIShortName, "ds", "upload"), + } +} + +func (ds UploadDataSource) Sync(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + ds.logger.Info("Sync", "cvi", cvi.Name) + + condition, _ := service.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + defer func() { service.SetCondition(condition, &cvi.Status.Conditions) }() + + supgen := supplements.NewGenerator(common.CVIShortName, cvi.Name, ds.controllerNamespace, cvi.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 cvi.Status.UploadCommand == "" { + if ing != nil && ing.Annotations[common.AnnUploadURL] != "" { + cvi.Status.UploadCommand = fmt.Sprintf("curl %s -T example.iso", ing.Annotations[common.AnnUploadURL]) + } + } + + switch { + case isDiskProvisioningFinished(condition): + ds.logger.Info("Finishing...", "cvi", cvi.Name) + + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.ReadyReason_Ready + condition.Message = "" + + cvi.Status.Phase = virtv2.ImageReady + + err = ds.uploaderService.Unprotect(ctx, pod, svc, ing) + if err != nil { + return false, err + } + + return CleanUp(ctx, cvi, ds) + case common.AnyTerminating(pod, svc, ing): + cvi.Status.Phase = virtv2.ImagePending + + ds.logger.Info("Cleaning up...", "cvi", cvi.Name) + case pod == nil && svc == nil && ing == nil: + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_Provisioning + condition.Message = "DVCR Provisioner not found: create the new one." + + envSettings := ds.getEnvSettings(supgen) + err = ds.uploaderService.Start(ctx, envSettings, cvi, supgen, datasource.NewCABundleForCVMI(cvi.Spec.DataSource)) + if err != nil { + return false, err + } + + cvi.Status.Phase = virtv2.ImagePending + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Create uploader pod...", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", nil) + case common.IsPodComplete(pod): + condition.Status = metav1.ConditionTrue + condition.Reason = cvicondition.ReadyReason_Ready + condition.Message = "" + + cvi.Status.Phase = virtv2.ImageReady + cvi.Status.Size = ds.statService.GetSize(pod) + cvi.Status.CDROM = ds.statService.GetCDROM(pod) + cvi.Status.Format = ds.statService.GetFormat(pod) + cvi.Status.Progress = "100%" + cvi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(cvi.GetUID(), pod) + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("Ready", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + case ds.statService.IsUploadStarted(cvi.GetUID(), pod): + err = ds.statService.CheckPod(pod) + if err != nil { + cvi.Status.Phase = virtv2.ImageFailed + + switch { + case errors.Is(err, service.ErrNotInitialized), errors.Is(err, service.ErrNotScheduled): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_ProvisioningNotStarted + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + case errors.Is(err, service.ErrProvisioningFailed): + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_ProvisioningFailed + condition.Message = service.CapitalizeFirstLetter(err.Error() + ".") + return false, nil + default: + return false, err + } + } + + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_Provisioning + condition.Message = "Import is in the process of provisioning to DVCR." + + cvi.Status.Phase = virtv2.ImageProvisioning + cvi.Status.Progress = ds.statService.GetProgress(cvi.GetUID(), pod, cvi.Status.Progress) + cvi.Status.DownloadSpeed = ds.statService.GetDownloadSpeed(cvi.GetUID(), pod) + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + err = ds.uploaderService.Protect(ctx, pod, svc, ing) + if err != nil { + return false, err + } + + ds.logger.Info("Provisioning...", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + default: + condition.Status = metav1.ConditionFalse + condition.Reason = cvicondition.ReadyReason_WaitForUserUpload + condition.Message = "Waiting for the user upload." + + cvi.Status.Phase = virtv2.ImageWaitForUserUpload + cvi.Status.Target.RegistryURL = ds.dvcrSettings.RegistryImageForCVMI(cvi.Name) + + ds.logger.Info("WaitForUserUpload...", "cvi", cvi.Name, "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + } + + return true, nil +} + +func (ds UploadDataSource) CleanUp(ctx context.Context, cvi *virtv2.ClusterVirtualImage) (bool, error) { + supgen := supplements.NewGenerator(common.CVIShortName, cvi.Name, ds.controllerNamespace, cvi.UID) + + requeue, err := ds.uploaderService.CleanUp(ctx, supgen) + if err != nil { + return false, err + } + + return requeue, nil +} + +func (ds UploadDataSource) Validate(_ context.Context, _ *virtv2.ClusterVirtualImage) 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/cvi/internal/source/upload_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go new file mode 100644 index 000000000..1c0b1d367 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go @@ -0,0 +1,144 @@ +/* +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 + +// var _ = Describe("UploadDataSource Run", func() { +// expectedStatus := getExpectedStatus() +// stat := getStatMock(expectedStatus) +// +// var uplr *UploaderMock +// +// var cvi *virtv2.ClusterVirtualImage +// +// BeforeEach(func() { +// cvi = &virtv2.ClusterVirtualImage{} +// cvi.Spec.DataSource.Type = virtv2.DataSourceTypeUpload +// uplr = &UploaderMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{}, nil +// }, +// GetServiceFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Service, error) { +// return &corev1.Service{}, nil +// }, +// GetIngressFunc: func(ctx context.Context, sup *supplements.Generator) (*netv1.Ingress, error) { +// return &netv1.Ingress{}, nil +// }, +// } +// }) +// +// It("To pending phase (no importer pod)", func() { +// uplr = &UploaderMock{ +// GetPodFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return nil, nil +// }, +// GetServiceFunc: func(ctx context.Context, sup *supplements.Generator) (*corev1.Service, error) { +// return nil, nil +// }, +// GetIngressFunc: func(ctx context.Context, sup *supplements.Generator) (*netv1.Ingress, error) { +// return nil, nil +// }, +// StartFunc: func(ctx context.Context, settings *uploader.Settings, obj service.ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { +// return nil +// }, +// } +// +// ds := NewUploadDataSource(stat, uplr, &dvcr.Settings{}, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImagePending)) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("To wait wo user upload phase", func() { +// uplr.GetPodFunc = func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{Phase: corev1.PodRunning}, +// }, nil +// } +// +// stat.IsUploadStartedFunc = func(ownerUID types.UID, pod *corev1.Pod) bool { +// return false +// } +// +// ds := NewUploadDataSource(stat, uplr, &dvcr.Settings{}, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageWaitForUserUpload)) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("To provisioning phase", func() { +// uplr.GetPodFunc = func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{Phase: corev1.PodRunning}, +// }, nil +// } +// +// stat.IsUploadStartedFunc = func(ownerUID types.UID, pod *corev1.Pod) bool { +// return true +// } +// +// ds := NewUploadDataSource(stat, uplr, &dvcr.Settings{}, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageProvisioning)) +// Expect(cvi.Status.Progress).To(Equal(expectedStatus.Progress)) +// Expect(cvi.Status.DownloadSpeed).To(Equal(expectedStatus.DownloadSpeed)) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("To ready phase", func() { +// uplr.GetPodFunc = func(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { +// return &corev1.Pod{ +// Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, +// }, nil +// } +// +// ds := NewUploadDataSource(stat, uplr, &dvcr.Settings{}, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// Expect(cvi.Status.Phase).To(Equal(virtv2.ImageReady)) +// Expect(cvi.Status.Size).To(Equal(expectedStatus.Size)) +// Expect(cvi.Status.CDROM).To(Equal(expectedStatus.CDROM)) +// Expect(cvi.Status.Format).To(Equal(expectedStatus.Format)) +// Expect(cvi.Status.Progress).To(Equal("100%")) +// Expect(cvi.Status.Target.RegistryURL).NotTo(BeEmpty()) +// }) +// +// It("Clean up", func() { +// cvi.Status.Phase = virtv2.ImageReady +// uplr := UploaderMock{ +// CleanUpFunc: func(ctx context.Context, sup *supplements.Generator) (bool, error) { +// return true, nil +// }, +// } +// +// ds := NewUploadDataSource(stat, &uplr, &dvcr.Settings{}, "") +// +// requeue, err := ds.Sync(context.Background(), cvi) +// Expect(err).To(BeNil()) +// Expect(requeue).To(BeTrue()) +// }) +// }) diff --git a/images/virtualization-artifact/pkg/controller/cvmi_controller.go b/images/virtualization-artifact/pkg/controller/cvmi_controller.go index c1708a222..270e8ac72 100644 --- a/images/virtualization-artifact/pkg/controller/cvmi_controller.go +++ b/images/virtualization-artifact/pkg/controller/cvmi_controller.go @@ -17,60 +17,10 @@ limitations under the License. package controller import ( - "context" - - "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" - "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 ( - cvmiControllerName = "cvi-controller" - cvmiShortName = "cvi" - ImporterPodVerbose = "3" ImporterPodPullPolicy = string(corev1.PullIfNotPresent) ) - -func NewCVMIController( - ctx context.Context, - mgr manager.Manager, - log logr.Logger, - importerImage string, - uploaderImage string, - controllerNamespace string, - dvcrSettings *dvcr.Settings, -) (controller.Controller, error) { - reconciler := NewCVMIReconciler( - importerImage, - uploaderImage, - ImporterPodVerbose, - ImporterPodPullPolicy, - dvcrSettings, - ) - - reconcilerCore := two_phase_reconciler.NewReconcilerCore[*CVMIReconcilerState]( - reconciler, - NewCVMIReconcilerState(controllerNamespace), - two_phase_reconciler.ReconcilerOptions{ - Client: mgr.GetClient(), - Cache: mgr.GetCache(), - Recorder: mgr.GetEventRecorderFor(cvmiControllerName), - Scheme: mgr.GetScheme(), - Log: log.WithName(cvmiControllerName), - }) - - cvmiController, err := controller.New(cvmiControllerName, mgr, controller.Options{Reconciler: reconcilerCore}) - if err != nil { - return nil, err - } - if err := reconciler.SetupController(ctx, mgr, cvmiController); err != nil { - return nil, err - } - log.Info("Initialized ClusterVirtualImage controller", "image", importerImage, "namespace", controllerNamespace) - return cvmiController, nil -} diff --git a/images/virtualization-artifact/pkg/controller/cvmi_importer.go b/images/virtualization-artifact/pkg/controller/cvmi_importer.go deleted file mode 100644 index 30584ab05..000000000 --- a/images/virtualization-artifact/pkg/controller/cvmi_importer.go +++ /dev/null @@ -1,122 +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" - - cvmiutil "github.com/deckhouse/virtualization-controller/pkg/common/cvmi" - "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/supplements" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" - virtv2alpha1 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -func (r *CVMIReconciler) startImporterPod( - ctx context.Context, - state *CVMIReconcilerState, - opts two_phase_reconciler.ReconcilerOptions, -) error { - cvmi := state.CVMI.Current() - opts.Log.V(1).Info("Creating importer POD for CVI", "cvi.Name", cvmi.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, cvmi, 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.NewCABundleForCVMI(cvmi.Spec.DataSource), r.dvcrSettings) -} - -// createImporterSettings fills settings for the dvcr-importer binary. -func (r *CVMIReconciler) createImporterSettings(state *CVMIReconcilerState) (*importer.Settings, error) { - cvmi := state.CVMI.Current() - settings := &importer.Settings{ - Verbose: r.verbose, - } - - ds := cvmi.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.ClusterVirtualImageObjectRefKindVirtualImage: - dvcrSourceImageName := r.dvcrSettings.RegistryImageForVMI(ds.ObjectRef.Name, ds.ObjectRef.Namespace) - importer.ApplyDVCRSourceSettings(settings, dvcrSourceImageName) - case virtv2alpha1.ClusterVirtualImageObjectRefKindClusterVirtualImage: - 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.RegistryImageForCVMI(cvmi.Name) - importer.ApplyDVCRDestinationSettings(settings, r.dvcrSettings, state.Supplements, dvcrDestImageName) - - // TODO Update proxy settings. - - return settings, nil -} - -func (r *CVMIReconciler) createImporterPodSettings(state *CVMIReconcilerState) *importer.PodSettings { - importerPod := state.Supplements.ImporterPod() - return &importer.PodSettings{ - Name: importerPod.Name, - Image: r.importerImage, - PullPolicy: r.pullPolicy, - Namespace: importerPod.Namespace, - OwnerReference: cvmiutil.MakeOwnerReference(state.CVMI.Current()), - ControllerName: cvmiControllerName, - InstallerLabels: map[string]string{}, - } -} diff --git a/images/virtualization-artifact/pkg/controller/cvmi_reconciler.go b/images/virtualization-artifact/pkg/controller/cvmi_reconciler.go deleted file mode 100644 index a9c66b4ef..000000000 --- a/images/virtualization-artifact/pkg/controller/cvmi_reconciler.go +++ /dev/null @@ -1,421 +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" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/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" - - cvmiutil "github.com/deckhouse/virtualization-controller/pkg/common/cvmi" - 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/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" - virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -type CVMIReconciler struct { - *vmattachee.AttacheeReconciler[*virtv2.ClusterVirtualImage, virtv2.ClusterVirtualImageStatus] - - importerImage string - uploaderImage string - verbose string - pullPolicy string - dvcrSettings *dvcr.Settings -} - -func NewCVMIReconciler(importerImage, uploaderImage, verbose, pullPolicy string, dvcrSettings *dvcr.Settings) *CVMIReconciler { - return &CVMIReconciler{ - importerImage: importerImage, - uploaderImage: uploaderImage, - verbose: verbose, - pullPolicy: pullPolicy, - dvcrSettings: dvcrSettings, - AttacheeReconciler: vmattachee.NewAttacheeReconciler[ - *virtv2.ClusterVirtualImage, - virtv2.ClusterVirtualImageStatus, - ](), - } -} - -func (r *CVMIReconciler) SetupController(ctx context.Context, mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch( - source.Kind(mgr.GetCache(), &virtv2.ClusterVirtualImage{}), - &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 err - } - - return r.AttacheeReconciler.SetupController(mgr, ctr, r) -} - -// Sync creates and deletes importer Pod depending on CVMI status. -func (r *CVMIReconciler) Sync(ctx context.Context, _ reconcile.Request, state *CVMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.Info("Reconcile required for CVI", "cvi.name", state.CVMI.Current().Name, "cvi.phase", state.CVMI.Current().Status.Phase) - - if r.AttacheeReconciler.Sync(ctx, state.AttacheeState, opts) { - return nil - } - - // Change the world depending on states of CVMI and Pod. - switch { - case state.IsDeletion(): - opts.Log.V(1).Info("Delete CVI, remove protective finalizers") - return r.cleanupOnDeletion(ctx, state, opts) - case !state.IsProtected(): - if err := r.verifyDataSourceRefs(ctx, opts.Client, state); err != nil { - return err - } - // Set protective finalizer atomically on a verified resource. - if controllerutil.AddFinalizer(state.CVMI.Changed(), virtv2.FinalizerCVMICleanup) { - state.SetReconcilerResult(&reconcile.Result{Requeue: true}) - return nil - } - case state.IsPodComplete(): - // Note: state.ShouldReconcile was positive, so state.Pod is not nil and should be deleted. - // Delete sub recourses (Pods, Services, Secrets) when CVMI is marked as ready and stop the reconcile process. - if cc.ShouldCleanupSubResources(state.CVMI.Current()) { - opts.Log.V(1).Info("Import done, cleanup") - return r.cleanup(ctx, state.CVMI.Changed(), opts.Client, state) - } - return nil - case state.IsFailed(): - opts.Log.Info("CVI failed: cleanup underlying resources") - // Delete underlying importer/uploader Pod, Service and DataVolume and stop the reconcile process. - if cc.ShouldCleanupSubResources(state.CVMI.Current()) { - return r.cleanup(ctx, state.CVMI.Changed(), opts.Client, state) - } - return nil - case state.CanStartPod(): - // Create Pod using name and namespace from annotation. - opts.Log.V(1).Info("Pod for CVI not found, create new one") - - if cvmiutil.IsDVCRSource(state.CVMI.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 - } - - 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.IsImportInProgress(), state.IsImportInPending(): - // Import is in progress, force a re-reconcile in 2 seconds to update status. - opts.Log.V(2).Info("Requeue: CVI import is in progress", "cvi.name", state.CVMI.Current().Name) - if err := r.ensurePodFinalizers(ctx, state, opts); err != nil { - return err - } - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - } - - // Report unexpected state. - details := fmt.Sprintf("cvi.Status.Phase='%s'", state.CVMI.Current().Status.Phase) - if state.Pod != nil { - details += fmt.Sprintf(" pod.Name='%s' pod.Status.Phase='%s'", state.Pod.Name, state.Pod.Status.Phase) - } - opts.Recorder.Event(state.CVMI.Current(), corev1.EventTypeWarning, virtv2.ReasonErrUnknownState, fmt.Sprintf("CVI has unexpected state, recreate it to start import again. %s", details)) - - return nil -} - -func (r *CVMIReconciler) UpdateStatus(ctx context.Context, _ reconcile.Request, state *CVMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.V(2).Info("Update CVI status") - - // Record event if 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.CVMI.Current(), corev1.EventTypeWarning, virtv2.ReasonErrImportFailed, fmt.Sprintf("pod phase '%s', message '%s'", state.Pod.Status.Phase, state.Pod.Status.ContainerStatuses[0].LastTerminationState.Terminated.Message)) - } - } - - cvmiStatus := state.CVMI.Current().Status.DeepCopy() - - // Set target image name the same way as for the importer/uploader Pod. - dvcrDestImageName := r.dvcrSettings.RegistryImageForCVMI(state.CVMI.Current().Name) - cvmiStatus.Target.RegistryURL = dvcrDestImageName - - switch { - case state.CVMI.Current().Status.Phase == "": - cvmiStatus.Phase = virtv2.ImagePending - if err := r.verifyDataSourceRefs(ctx, opts.Client, state); err != nil { - cvmiStatus.FailureReason = FailureReasonCannotBeProcessed - cvmiStatus.FailureMessage = fmt.Sprintf("DataSource is invalid. %s", err) - } - case state.IsReady(), state.IsFailed(): - break - case !state.IsPodComplete(): - // Set CVMI status to Provisioning and copy progress metrics from importer/uploader Pod. - opts.Log.V(2).Info("Fetch progress", "cvi.name", state.CVMI.Current().Name) - cvmiStatus.Phase = virtv2.ImageProvisioning - if state.CVMI.Current().Spec.DataSource.Type == virtv2.DataSourceTypeUpload && - cvmiStatus.UploadCommand == "" && - state.Ingress != nil && - state.Ingress.GetAnnotations()[cc.AnnUploadURL] != "" { - cvmiStatus.UploadCommand = fmt.Sprintf( - "curl %s -T example.iso", - state.Ingress.GetAnnotations()[cc.AnnUploadURL], - ) - } - var progress *monitoring.ImportProgress - if !cvmiutil.IsDVCRSource(state.CVMI.Current()) { - var err error - progress, err = monitoring.GetImportProgressFromPod(string(state.CVMI.Current().GetUID()), state.Pod) - if err != nil { - opts.Recorder.Event(state.CVMI.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", "cvi.name", state.CVMI.Current().Name, "progress", progress.Progress(), "speed", progress.AvgSpeed(), "progress.raw", progress.ProgressRaw(), "speed.raw", progress.AvgSpeedRaw()) - cvmiStatus.Progress = progress.Progress() - cvmiStatus.DownloadSpeed.Avg = progress.AvgSpeed() - cvmiStatus.DownloadSpeed.AvgBytes = strconv.FormatUint(progress.AvgSpeedRaw(), 10) - cvmiStatus.DownloadSpeed.Current = progress.CurSpeed() - cvmiStatus.DownloadSpeed.CurrentBytes = strconv.FormatUint(progress.CurSpeedRaw(), 10) - } - } - // Set CVMI phase. - if state.CVMI.Current().Spec.DataSource.Type == virtv2.DataSourceTypeUpload && (progress == nil || progress.ProgressRaw() == 0) { - cvmiStatus.Phase = virtv2.ImageWaitForUserUpload - if state.Pod != nil && helper.GetAge(state.Pod) > cc.UploaderWaitDuration { - cvmiStatus.Phase = virtv2.ImageFailed - cvmiStatus.FailureReason = virtv2.ReasonErrUploaderWaitDurationExpired - cvmiStatus.FailureMessage = "uploading time expired" - } - } else { - cvmiStatus.Phase = virtv2.ImageProvisioning - } - case state.IsPodComplete(): - // Set CVMI status to Ready and update image size from final report of the importer/uploader Pod. - opts.Recorder.Event(state.CVMI.Current(), corev1.EventTypeNormal, virtv2.ReasonImportSucceeded, "Import Successful") - opts.Log.V(1).Info("Import completed successfully") - cvmiStatus.Phase = virtv2.ImageReady - cvmiStatus.Progress = "100%" - // Cleanup. - cvmiStatus.DownloadSpeed.Current = "" - cvmiStatus.DownloadSpeed.CurrentBytes = "" - - switch { - case cvmiutil.IsDVCRSource(state.CVMI.Current()): - cvmiStatus.Format = state.DVCRDataSource.GetFormat() - cvmiStatus.CDROM = imageformat.IsISO(cvmiStatus.Format) - cvmiStatus.Size = state.DVCRDataSource.GetSize() - default: - finalReport, err := monitoring.GetFinalReportFromPod(state.Pod) - if err != nil { - return err - } - - if finalReport.ErrMessage != "" { - cvmiStatus.Phase = virtv2.ImageFailed - cvmiStatus.FailureReason = virtv2.ReasonErrImportFailed - cvmiStatus.FailureMessage = finalReport.ErrMessage - opts.Recorder.Event(state.CVMI.Current(), corev1.EventTypeWarning, virtv2.ReasonErrImportFailed, finalReport.ErrMessage) - break - } - - cvmiStatus.Format = finalReport.Format - cvmiStatus.CDROM = imageformat.IsISO(cvmiStatus.Format) - cvmiStatus.DownloadSpeed.Avg = finalReport.GetAverageSpeed() - cvmiStatus.DownloadSpeed.AvgBytes = strconv.FormatUint(finalReport.GetAverageSpeedRaw(), 10) - cvmiStatus.Size.Stored = finalReport.StoredSize() - cvmiStatus.Size.StoredBytes = strconv.FormatUint(finalReport.StoredSizeBytes, 10) - cvmiStatus.Size.Unpacked = finalReport.UnpackedSize() - cvmiStatus.Size.UnpackedBytes = strconv.FormatUint(finalReport.UnpackedSizeBytes, 10) - } - } - - state.CVMI.Changed().Status = *cvmiStatus - - return nil -} - -func (r *CVMIReconciler) verifyDataSourceRefs(ctx context.Context, client client.Client, state *CVMIReconcilerState) error { - cvmi := state.CVMI.Current() - switch cvmi.Spec.DataSource.Type { - case virtv2.DataSourceTypeObjectRef: - if err := state.DVCRDataSource.Validate(); err != nil { - return err - } - case virtv2.DataSourceTypeContainerImage: - if cvmi.Spec.DataSource.ContainerImage == nil { - return fmt.Errorf("dataSource '%s' specified without related 'containerImage' section", cvmi.Spec.DataSource.Type) - } - if cvmi.Spec.DataSource.ContainerImage.ImagePullSecret.Name != "" { - ns := cvmi.Spec.DataSource.ContainerImage.ImagePullSecret.Namespace - if ns == "" { - ns = cvmi.GetNamespace() - } - secretName := types.NamespacedName{ - Namespace: ns, - Name: cvmi.Spec.DataSource.ContainerImage.ImagePullSecret.Name, - } - srcSecret, err := helper.FetchObject[*corev1.Secret](ctx, secretName, client, &corev1.Secret{}) - if err != nil || srcSecret == nil { - return fmt.Errorf("containerImage.imagePullSecret %s not found", secretName.String()) - } - } - } - return nil -} - -func (r *CVMIReconciler) cleanup(ctx context.Context, cvmi *virtv2.ClusterVirtualImage, client client.Client, state *CVMIReconcilerState) error { - switch cvmi.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 *CVMIReconciler) startPod( - ctx context.Context, - state *CVMIReconcilerState, - opts two_phase_reconciler.ReconcilerOptions, -) error { - switch state.CVMI.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 -} - -// ensurePodFinalizers adds protective finalizers on importer/uploader Pod and Service dependencies. -func (r *CVMIReconciler) ensurePodFinalizers(ctx context.Context, state *CVMIReconcilerState, 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 -} - -func (r *CVMIReconciler) ShouldDeleteChildResources(state *CVMIReconcilerState) bool { - return state.Pod != nil || state.Service != nil || state.Ingress != nil -} - -func (r *CVMIReconciler) cleanupOnDeletion(ctx context.Context, state *CVMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - if r.ShouldDeleteChildResources(state) { - if err := r.cleanup(ctx, state.CVMI.Current(), opts.Client, state); err != nil { - return err - } - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - } - controllerutil.RemoveFinalizer(state.CVMI.Changed(), virtv2.FinalizerCVMICleanup) - return nil -} - -func (r *CVMIReconciler) FilterAttachedVM(vm *virtv2.VirtualMachine) bool { - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind == virtv2.ClusterImageDevice { - return true - } - } - - return false -} - -func (r *CVMIReconciler) EnqueueFromAttachedVM(vm *virtv2.VirtualMachine) []reconcile.Request { - var requests []reconcile.Request - - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind != virtv2.ClusterImageDevice { - continue - } - - requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ - Name: bda.Name, - }}) - } - - return requests -} diff --git a/images/virtualization-artifact/pkg/controller/cvmi_reconciler_state.go b/images/virtualization-artifact/pkg/controller/cvmi_reconciler_state.go deleted file mode 100644 index 0d87dbc73..000000000 --- a/images/virtualization-artifact/pkg/controller/cvmi_reconciler_state.go +++ /dev/null @@ -1,217 +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" - "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" - - 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 CVMIReconcilerState struct { - *vmattachee.AttacheeState[*virtv2.ClusterVirtualImage, virtv2.ClusterVirtualImageStatus] - - Client client.Client - Supplements *supplements.Generator - Result *reconcile.Result - Namespace string - - CVMI *helper.Resource[*virtv2.ClusterVirtualImage, virtv2.ClusterVirtualImageStatus] - Service *corev1.Service - Ingress *netv1.Ingress - Pod *corev1.Pod - DVCRDataSource *DVCRDataSource -} - -func NewCVMIReconcilerState(controllerNamespace string) func(name types.NamespacedName, log logr.Logger, client client.Client, cache cache.Cache) *CVMIReconcilerState { - return func(name types.NamespacedName, log logr.Logger, client client.Client, cache cache.Cache) *CVMIReconcilerState { - state := &CVMIReconcilerState{ - Client: client, - CVMI: helper.NewResource( - name, log, client, cache, - func() *virtv2.ClusterVirtualImage { return &virtv2.ClusterVirtualImage{} }, - func(obj *virtv2.ClusterVirtualImage) virtv2.ClusterVirtualImageStatus { - return obj.Status - }, - ), - Namespace: controllerNamespace, - } - state.AttacheeState = vmattachee.NewAttacheeState( - state, - virtv2.FinalizerCVMIProtection, - state.CVMI, - ) - return state - } -} - -func (state *CVMIReconcilerState) ApplySync(ctx context.Context, _ logr.Logger) error { - if err := state.CVMI.UpdateMeta(ctx); err != nil { - return fmt.Errorf("unable to update CVI %q meta: %w", state.CVMI.Name(), err) - } - return nil -} - -func (state *CVMIReconcilerState) ApplyUpdateStatus(ctx context.Context, _ logr.Logger) error { - return state.CVMI.UpdateStatus(ctx) -} - -func (state *CVMIReconcilerState) SetReconcilerResult(result *reconcile.Result) { - state.Result = result -} - -func (state *CVMIReconcilerState) GetReconcilerResult() *reconcile.Result { - return state.Result -} - -func (state *CVMIReconcilerState) Reload(ctx context.Context, req reconcile.Request, log logr.Logger, client client.Client) error { - err := state.CVMI.Fetch(ctx) - if err != nil { - return fmt.Errorf("unable to get %q: %w", req.NamespacedName, err) - } - if state.CVMI.IsEmpty() { - log.Info("Reconcile observe an absent CVI: it may be deleted", "cvi", req.NamespacedName) - return nil - } - - state.Supplements = &supplements.Generator{ - Prefix: cvmiShortName, - Name: state.CVMI.Current().Name, - Namespace: state.Namespace, - UID: state.CVMI.Current().UID, - } - - ds := state.CVMI.Current().Spec.DataSource - - switch ds.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 = NewDVCRDataSourcesForCVMI(ctx, state.CVMI.Current().Spec.DataSource, client) - if err != nil { - return err - } - } - - return state.AttacheeState.Reload(ctx, req, log, client) -} - -// ShouldReconcile tells if Sync and UpdateStatus should run. -func (state *CVMIReconcilerState) ShouldReconcile(log logr.Logger) bool { - // CVMI was not found. E.g. CVMI was deleted, but requeue task was triggered. - if state.CVMI.IsEmpty() { - return false - } - if state.AttacheeState.ShouldReconcile(log) { - return true - } - return true -} - -func (state *CVMIReconcilerState) IsFailed() bool { - if state.CVMI.IsEmpty() { - return false - } - return state.CVMI.Current().Status.Phase == virtv2.ImageFailed -} - -func (state *CVMIReconcilerState) IsProtected() bool { - return controllerutil.ContainsFinalizer(state.CVMI.Current(), virtv2.FinalizerCVMICleanup) -} - -func (state *CVMIReconcilerState) IsDeletion() bool { - if state.CVMI.IsEmpty() { - return false - } - return state.CVMI.Current().DeletionTimestamp != nil -} - -func (state *CVMIReconcilerState) IsImportInProgress() bool { - return state.Pod != nil && state.Pod.Status.Phase == corev1.PodRunning -} - -func (state *CVMIReconcilerState) IsImportInPending() bool { - return state.Pod != nil && state.Pod.Status.Phase == corev1.PodPending -} - -// CanStartPod returns whether importer Pod can be started. -// NOTE: valid only if ShouldTrackPod is true. -func (state *CVMIReconcilerState) CanStartPod() bool { - return !state.IsReady() && !state.IsFailed() && state.Pod == nil -} - -func (state *CVMIReconcilerState) IsReady() bool { - if state.CVMI.IsEmpty() { - return false - } - return state.CVMI.Current().Status.Phase == virtv2.ImageReady -} - -// IsPodComplete returns whether importer/uploader Pod was completed. -// NOTE: valid only if ShouldTrackPod is true. -func (state *CVMIReconcilerState) IsPodComplete() bool { - return state.Pod != nil && cc.IsPodComplete(state.Pod) -} - -func (state *CVMIReconcilerState) IsAttachedToVM(vm virtv2.VirtualMachine) bool { - if state.CVMI.IsEmpty() { - return false - } - - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind == virtv2.ClusterImageDevice && bda.Name == state.CVMI.Name().Name { - return true - } - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/cvmi_uploader.go b/images/virtualization-artifact/pkg/controller/cvmi_uploader.go deleted file mode 100644 index d1066feb8..000000000 --- a/images/virtualization-artifact/pkg/controller/cvmi_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" - - cvmiutil "github.com/deckhouse/virtualization-controller/pkg/common/cvmi" - "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - 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 *CVMIReconciler) startUploaderPod(ctx context.Context, state *CVMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - cvmi := state.CVMI.Current() - - opts.Log.V(1).Info("Creating uploader POD for CVI", "cvi.Name", cvmi.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, cvmi, 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.NewCABundleForCVMI(cvmi.Spec.DataSource), r.dvcrSettings) -} - -// createUploaderSettings fills settings for the dvcr-uploader binary. -func (r *CVMIReconciler) createUploaderSettings(state *CVMIReconcilerState) *uploader.Settings { - settings := &uploader.Settings{ - Verbose: r.verbose, - } - - // Set DVCR destination settings. - dvcrDestImageName := r.dvcrSettings.RegistryImageForCVMI(state.CVMI.Current().Name) - uploader.ApplyDVCRDestinationSettings(settings, r.dvcrSettings, state.Supplements, dvcrDestImageName) - - // TODO Update proxy settings. - - return settings -} - -func (r *CVMIReconciler) createUploaderPodSettings(state *CVMIReconcilerState) *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: cvmiutil.MakeOwnerReference(state.CVMI.Current()), - ControllerName: cvmiControllerName, - InstallerLabels: map[string]string{}, - ServiceName: uploaderSvc.Name, - } -} - -func (r *CVMIReconciler) startUploaderService(ctx context.Context, state *CVMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.V(1).Info("Creating uploader Service for CVI", "cvi.Name", state.CVMI.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 *CVMIReconciler) createUploaderServiceSettings(state *CVMIReconcilerState) *uploader.ServiceSettings { - uploaderSvc := state.Supplements.UploaderService() - return &uploader.ServiceSettings{ - Name: uploaderSvc.Name, - Namespace: uploaderSvc.Namespace, - OwnerReference: cvmiutil.MakeOwnerReference(state.CVMI.Current()), - } -} - -func (r *CVMIReconciler) startUploaderIngress(ctx context.Context, state *CVMIReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.V(1).Info("Creating uploader Ingress for CVI", "cvi.Name", state.CVMI.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 *CVMIReconciler) createUploaderIngressSettings(state *CVMIReconcilerState) *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: cvmiutil.MakeOwnerReference(state.CVMI.Current()), - } -} diff --git a/images/virtualization-artifact/pkg/controller/dvcr_data_source.go b/images/virtualization-artifact/pkg/controller/dvcr_data_source.go index 3c51faa0f..338b0677c 100644 --- a/images/virtualization-artifact/pkg/controller/dvcr_data_source.go +++ b/images/virtualization-artifact/pkg/controller/dvcr_data_source.go @@ -34,6 +34,7 @@ type DVCRDataSource struct { size virtv2.ImageStatusSize meta metav1.Object format string + target string isReady bool } @@ -59,6 +60,7 @@ func NewDVCRDataSourcesForCVMI(ctx context.Context, ds virtv2.ClusterVirtualImag dsDVCR.format = vmi.Status.Format dsDVCR.meta = vmi.GetObjectMeta() dsDVCR.isReady = vmi.Status.Phase == virtv2.ImageReady + dsDVCR.target = vmi.Status.Target.RegistryURL } } case virtv2.ClusterVirtualImageObjectRefKindClusterVirtualImage: @@ -74,6 +76,7 @@ func NewDVCRDataSourcesForCVMI(ctx context.Context, ds virtv2.ClusterVirtualImag dsDVCR.meta = cvmi.GetObjectMeta() dsDVCR.format = cvmi.Status.Format dsDVCR.isReady = cvmi.Status.Phase == virtv2.ImageReady + dsDVCR.target = cvmi.Status.Target.RegistryURL } } } @@ -103,6 +106,7 @@ func NewDVCRDataSourcesForVMI(ctx context.Context, ds virtv2.VirtualImageDataSou dsDVCR.format = vmi.Status.Format dsDVCR.meta = vmi.GetObjectMeta() dsDVCR.isReady = vmi.Status.Phase == virtv2.ImageReady + dsDVCR.target = vmi.Status.Target.RegistryURL } } case virtv2.VirtualImageObjectRefKindClusterVirtualImage: @@ -118,6 +122,7 @@ func NewDVCRDataSourcesForVMI(ctx context.Context, ds virtv2.VirtualImageDataSou dsDVCR.meta = cvmi.GetObjectMeta() dsDVCR.format = cvmi.Status.Format dsDVCR.isReady = cvmi.Status.Phase == virtv2.ImageReady + dsDVCR.target = cvmi.Status.Target.RegistryURL } } } @@ -148,6 +153,7 @@ func NewDVCRDataSourcesForVMD(ctx context.Context, ds *virtv2.VirtualDiskDataSou dsDVCR.format = vmi.Status.Format dsDVCR.meta = vmi.GetObjectMeta() dsDVCR.isReady = vmi.Status.Phase == virtv2.ImageReady + dsDVCR.target = vmi.Status.Target.RegistryURL } } case virtv2.VirtualDiskObjectRefKindClusterVirtualImage: @@ -163,6 +169,7 @@ func NewDVCRDataSourcesForVMD(ctx context.Context, ds *virtv2.VirtualDiskDataSou dsDVCR.meta = cvmi.GetObjectMeta() dsDVCR.format = cvmi.Status.Format dsDVCR.isReady = cvmi.Status.Phase == virtv2.ImageReady + dsDVCR.target = cvmi.Status.Target.RegistryURL } } } @@ -189,3 +196,7 @@ func (ds *DVCRDataSource) GetFormat() string { func (ds *DVCRDataSource) IsReady() bool { return ds.isReady } + +func (ds *DVCRDataSource) GetTarget() string { + return ds.target +} diff --git a/images/virtualization-artifact/pkg/controller/importer/importer_pod.go b/images/virtualization-artifact/pkg/controller/importer/importer_pod.go index d791432e7..20ed812a5 100644 --- a/images/virtualization-artifact/pkg/controller/importer/importer_pod.go +++ b/images/virtualization-artifact/pkg/controller/importer/importer_pod.go @@ -61,13 +61,13 @@ const ( type Importer struct { PodSettings *PodSettings - Settings *Settings + EnvSettings *Settings } -func NewImporter(podSettings *PodSettings, settings *Settings) *Importer { +func NewImporter(podSettings *PodSettings, envSettings *Settings) *Importer { return &Importer{ PodSettings: podSettings, - Settings: settings, + EnvSettings: envSettings, } } @@ -175,7 +175,7 @@ func (imp *Importer) makeImporterContainerSpec() *corev1.Container { Image: imp.PodSettings.Image, ImagePullPolicy: corev1.PullPolicy(imp.PodSettings.PullPolicy), Command: []string{"sh"}, - Args: []string{"/importer_entrypoint.sh", "-v=" + imp.Settings.Verbose}, + Args: []string{"/importer_entrypoint.sh", "-v=" + imp.EnvSettings.Verbose}, Ports: []corev1.ContainerPort{ { Name: "metrics", @@ -198,19 +198,19 @@ func (imp *Importer) makeImporterContainerEnv() []corev1.EnvVar { env := []corev1.EnvVar{ { Name: common.ImporterSource, - Value: imp.Settings.Source, + Value: imp.EnvSettings.Source, }, { Name: common.ImporterEndpoint, - Value: imp.Settings.Endpoint, + Value: imp.EnvSettings.Endpoint, }, { Name: common.ImporterContentType, - Value: imp.Settings.ContentType, + Value: imp.EnvSettings.ContentType, }, { Name: common.ImporterImageSize, - Value: imp.Settings.ImageSize, + Value: imp.EnvSettings.ImageSize, }, { Name: common.OwnerUID, @@ -218,47 +218,47 @@ func (imp *Importer) makeImporterContainerEnv() []corev1.EnvVar { }, { Name: common.FilesystemOverheadVar, - Value: imp.Settings.FilesystemOverhead, + Value: imp.EnvSettings.FilesystemOverhead, }, { Name: common.InsecureTLSVar, - Value: strconv.FormatBool(imp.Settings.InsecureTLS), + Value: strconv.FormatBool(imp.EnvSettings.InsecureTLS), }, { Name: common.ImporterDiskID, - Value: imp.Settings.DiskID, + Value: imp.EnvSettings.DiskID, }, { Name: common.ImporterUUID, - Value: imp.Settings.UUID, + Value: imp.EnvSettings.UUID, }, { Name: common.ImporterReadyFile, - Value: imp.Settings.ReadyFile, + Value: imp.EnvSettings.ReadyFile, }, { Name: common.ImporterDoneFile, - Value: imp.Settings.DoneFile, + Value: imp.EnvSettings.DoneFile, }, { Name: common.ImporterBackingFile, - Value: imp.Settings.BackingFile, + Value: imp.EnvSettings.BackingFile, }, { Name: common.ImporterThumbprint, - Value: imp.Settings.Thumbprint, + Value: imp.EnvSettings.Thumbprint, }, { Name: common.ImportProxyHTTP, - Value: imp.Settings.HTTPProxy, + Value: imp.EnvSettings.HTTPProxy, }, { Name: common.ImportProxyHTTPS, - Value: imp.Settings.HTTPSProxy, + Value: imp.EnvSettings.HTTPSProxy, }, { Name: common.ImportProxyNoProxy, - Value: imp.Settings.NoProxy, + Value: imp.EnvSettings.NoProxy, }, } @@ -266,36 +266,36 @@ func (imp *Importer) makeImporterContainerEnv() []corev1.EnvVar { env = append(env, []corev1.EnvVar{ { Name: common.ImporterDestinationEndpoint, - Value: imp.Settings.DestinationEndpoint, + Value: imp.EnvSettings.DestinationEndpoint, }, { Name: common.DestinationInsecureTLSVar, - Value: imp.Settings.DestinationInsecureTLS, + Value: imp.EnvSettings.DestinationInsecureTLS, }, }...) // HTTP source checksum settings: md5 and sha256. - if imp.Settings.SHA256 != "" { + if imp.EnvSettings.SHA256 != "" { env = append(env, corev1.EnvVar{ Name: common.ImporterSHA256Sum, - Value: imp.Settings.SHA256, + Value: imp.EnvSettings.SHA256, }) } - if imp.Settings.MD5 != "" { + if imp.EnvSettings.MD5 != "" { env = append(env, corev1.EnvVar{ Name: common.ImporterMD5Sum, - Value: imp.Settings.MD5, + Value: imp.EnvSettings.MD5, }) } // Pass basic auth configuration from Secret with downward API. - if imp.Settings.SecretName != "" { + if imp.EnvSettings.SecretName != "" { env = append(env, corev1.EnvVar{ Name: common.ImporterAccessKeyID, ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: imp.Settings.SecretName, + Name: imp.EnvSettings.SecretName, }, Key: common.KeyAccess, }, @@ -305,7 +305,7 @@ func (imp *Importer) makeImporterContainerEnv() []corev1.EnvVar { ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: imp.Settings.SecretName, + Name: imp.EnvSettings.SecretName, }, Key: common.KeySecret, }, @@ -318,10 +318,10 @@ func (imp *Importer) makeImporterContainerEnv() []corev1.EnvVar { // addVolumes fills Volumes in Pod spec and VolumeMounts and envs in container spec. func (imp *Importer) addVolumes(pod *corev1.Pod, container *corev1.Container) { - if imp.Settings.AuthSecret != "" { + if imp.EnvSettings.AuthSecret != "" { // Mount source registry auth Secret and pass directory with mounted source registry login config. podutil.AddVolume(pod, container, - podutil.CreateSecretVolume(sourceRegistryAuthVol, imp.Settings.AuthSecret), + podutil.CreateSecretVolume(sourceRegistryAuthVol, imp.EnvSettings.AuthSecret), podutil.CreateVolumeMount(sourceRegistryAuthVol, common.ImporterAuthConfigDir), corev1.EnvVar{ Name: common.ImporterAuthConfigVar, @@ -330,10 +330,10 @@ func (imp *Importer) addVolumes(pod *corev1.Pod, container *corev1.Container) { ) } - if imp.Settings.DestinationAuthSecret != "" { + if imp.EnvSettings.DestinationAuthSecret != "" { // Mount DVCR auth Secret and pass directory with mounted DVCR login config. podutil.AddVolume(pod, container, - podutil.CreateSecretVolume(destinationAuthVol, imp.Settings.DestinationAuthSecret), + podutil.CreateSecretVolume(destinationAuthVol, imp.EnvSettings.DestinationAuthSecret), podutil.CreateVolumeMount(destinationAuthVol, common.ImporterDestinationAuthConfigDir), corev1.EnvVar{ Name: common.ImporterDestinationAuthConfigVar, @@ -343,9 +343,9 @@ func (imp *Importer) addVolumes(pod *corev1.Pod, container *corev1.Container) { } // Volume with CA certificates either from caBundle field or from existing ConfigMap. - if imp.Settings.CertConfigMap != "" { + if imp.EnvSettings.CertConfigMap != "" { podutil.AddVolume(pod, container, - podutil.CreateConfigMapVolume(certVolName, imp.Settings.CertConfigMap), + podutil.CreateConfigMapVolume(certVolName, imp.EnvSettings.CertConfigMap), podutil.CreateVolumeMount(certVolName, common.ImporterCertDir), corev1.EnvVar{ Name: common.ImporterCertDirVar, @@ -354,9 +354,9 @@ func (imp *Importer) addVolumes(pod *corev1.Pod, container *corev1.Container) { ) } - if imp.Settings.CertConfigMapProxy != "" { + if imp.EnvSettings.CertConfigMapProxy != "" { podutil.AddVolume(pod, container, - podutil.CreateConfigMapVolume(proxyCertVolName, imp.Settings.CertConfigMapProxy), // GetImportProxyConfigMapName(args.cvmi.Name) + podutil.CreateConfigMapVolume(proxyCertVolName, imp.EnvSettings.CertConfigMapProxy), // GetImportProxyConfigMapName(args.cvmi.Name) podutil.CreateVolumeMount(proxyCertVolName, common.ImporterProxyCertDir), corev1.EnvVar{ Name: common.ImporterProxyCertDirVar, @@ -366,7 +366,7 @@ func (imp *Importer) addVolumes(pod *corev1.Pod, container *corev1.Container) { } // Mount extra headers Secrets. - for index, header := range imp.Settings.SecretExtraHeaders { + for index, header := range imp.EnvSettings.SecretExtraHeaders { volName := fmt.Sprintf(secretExtraHeadersVolumeName, index) mountPath := path.Join(common.ImporterSecretExtraHeadersDir, fmt.Sprint(index)) envName := fmt.Sprintf("%s%d", common.ImporterExtraHeader, index) diff --git a/images/virtualization-artifact/pkg/controller/monitoring/final_report.go b/images/virtualization-artifact/pkg/controller/monitoring/final_report.go index 2c2524db7..d5669f796 100644 --- a/images/virtualization-artifact/pkg/controller/monitoring/final_report.go +++ b/images/virtualization-artifact/pkg/controller/monitoring/final_report.go @@ -51,13 +51,15 @@ func (r *FinalReport) GetAverageSpeedRaw() uint64 { return r.AverageSpeed } +var ErrTerminationMessageNotFound = errors.New("termination message not found in the Pod status") + func GetFinalReportFromPod(pod *corev1.Pod) (*FinalReport, error) { if pod == nil { return nil, errors.New("got nil Pod: unable to get the final report from the nil Pod") } if len(pod.Status.ContainerStatuses) == 0 || pod.Status.ContainerStatuses[0].State.Terminated == nil { - return nil, errors.New("termination message not found in the Pod status") + return nil, ErrTerminationMessageNotFound } message := pod.Status.ContainerStatuses[0].State.Terminated.Message diff --git a/images/virtualization-artifact/pkg/controller/service/condition.go b/images/virtualization-artifact/pkg/controller/service/condition.go new file mode 100644 index 000000000..7b0203188 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/condition.go @@ -0,0 +1,62 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "unicode" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +func GetCondition(condType cvicondition.Type, conds []metav1.Condition) (metav1.Condition, bool) { + for _, cond := range conds { + if cond.Type == condType { + return cond, true + } + } + + return metav1.Condition{}, false +} + +func SetCondition(cond metav1.Condition, conditions *[]metav1.Condition) { + if conditions == nil { + return + } + + for i := range *conditions { + if (*conditions)[i].Type == cond.Type { + (*conditions)[i] = cond + return + } + } + + *conditions = append(*conditions, cond) +} + +func CapitalizeFirstLetter(s string) string { + if s == "" { + return "" + } + + // Convert the first rune to uppercase and append the rest of the string. + runes := []rune(s) + runes[0] = unicode.ToUpper(runes[0]) + + return string(runes) +} diff --git a/images/virtualization-artifact/pkg/controller/service/importer_service.go b/images/virtualization-artifact/pkg/controller/service/importer_service.go new file mode 100644 index 000000000..806b599a7 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/importer_service.go @@ -0,0 +1,131 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + 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/importer" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" +) + +type ImporterService struct { + dvcrSettings *dvcr.Settings + client client.Client + image string + pullPolicy string + verbose string + controllerName string + protection *ProtectionService +} + +func NewImporterService( + dvcrSettings *dvcr.Settings, + client client.Client, + image string, + pullPolicy string, + verbose string, + controllerName string, + protection *ProtectionService, +) *ImporterService { + return &ImporterService{ + dvcrSettings: dvcrSettings, + client: client, + image: image, + pullPolicy: pullPolicy, + verbose: verbose, + controllerName: controllerName, + protection: protection, + } +} + +func (s ImporterService) Start(ctx context.Context, settings *importer.Settings, obj ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { + ownerRef := metav1.NewControllerRef(obj, obj.GroupVersionKind()) + settings.Verbose = s.verbose + + pod, err := importer.NewImporter(s.getPodSettings(ownerRef, sup), settings).CreatePod(ctx, s.client) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return supplements.EnsureForPod(ctx, s.client, sup, pod, caBundle, s.dvcrSettings) +} + +func (s ImporterService) CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) { + return s.CleanUpSubResources(ctx, sup) +} + +func (s ImporterService) CleanUpSubResources(ctx context.Context, sup *supplements.Generator) (bool, error) { + pod, err := s.GetPod(ctx, sup) + if err != nil { + return false, err + } + + err = s.protection.RemoveProtection(ctx, pod) + if err != nil { + return false, err + } + + var hasDeleted bool + + if pod != nil { + hasDeleted = true + err = s.client.Delete(ctx, pod) + if err != nil && !k8serrors.IsNotFound(err) { + return false, err + } + } + + return hasDeleted, nil +} + +func (s ImporterService) Protect(ctx context.Context, pod *corev1.Pod) error { + return s.protection.AddProtection(ctx, pod) +} + +func (s ImporterService) Unprotect(ctx context.Context, pod *corev1.Pod) error { + return s.protection.RemoveProtection(ctx, pod) +} + +func (s ImporterService) GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { + pod, err := importer.FindPod(ctx, s.client, sup) + if err != nil { + return nil, err + } + + return pod, nil +} + +func (s ImporterService) getPodSettings(ownerRef *metav1.OwnerReference, sup *supplements.Generator) *importer.PodSettings { + importerPod := sup.ImporterPod() + return &importer.PodSettings{ + Name: importerPod.Name, + Namespace: importerPod.Namespace, + Image: s.image, + PullPolicy: s.pullPolicy, + OwnerReference: *ownerRef, + ControllerName: s.controllerName, + InstallerLabels: map[string]string{}, + } +} diff --git a/images/virtualization-artifact/pkg/controller/service/interfaces.go b/images/virtualization-artifact/pkg/controller/service/interfaces.go new file mode 100644 index 000000000..6e2b2be52 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/interfaces.go @@ -0,0 +1,27 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type ObjectKind interface { + metav1.Object + schema.ObjectKind +} diff --git a/images/virtualization-artifact/pkg/controller/service/protection_service.go b/images/virtualization-artifact/pkg/controller/service/protection_service.go new file mode 100644 index 000000000..a1126e4a7 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/protection_service.go @@ -0,0 +1,111 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "context" + "reflect" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type ProtectionService struct { + client client.Client + finalizer string +} + +func NewProtectionService(client client.Client, finalizer string) *ProtectionService { + return &ProtectionService{ + client: client, + finalizer: finalizer, + } +} + +func (s ProtectionService) AddOwnerRef(ctx context.Context, owner client.Object, objs ...client.Object) error { + if owner == nil || reflect.ValueOf(owner).IsNil() { + return nil + } + + for _, obj := range objs { + if obj == nil || reflect.ValueOf(obj).IsNil() { + continue + } + + err := controllerutil.SetOwnerReference(owner, obj, s.client.Scheme()) + if err != nil { + return err + } + + var patch client.Patch + patch, err = GetPatchOwnerReferences(obj.GetOwnerReferences()) + if err != nil { + return err + } + + err = s.client.Patch(ctx, obj, patch) + if err != nil { + return err + } + } + + return nil +} + +func (s ProtectionService) AddProtection(ctx context.Context, objs ...client.Object) error { + for _, obj := range objs { + if obj == nil || reflect.ValueOf(obj).IsNil() { + continue + } + + if controllerutil.AddFinalizer(obj, s.finalizer) { + patch, err := GetPatchFinalizers(obj.GetFinalizers()) + if err != nil { + return err + } + + err = s.client.Patch(ctx, obj, patch) + if err != nil { + return err + } + } + } + + return nil +} + +func (s ProtectionService) RemoveProtection(ctx context.Context, objs ...client.Object) error { + for _, obj := range objs { + if obj == nil || reflect.ValueOf(obj).IsNil() { + continue + } + + if controllerutil.RemoveFinalizer(obj, s.finalizer) { + patch, err := GetPatchFinalizers(obj.GetFinalizers()) + if err != nil { + return err + } + + err = s.client.Patch(ctx, obj, patch) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/service/resource.go b/images/virtualization-artifact/pkg/controller/service/resource.go new file mode 100644 index 000000000..fc91f9f4e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/resource.go @@ -0,0 +1,159 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/strings/slices" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" +) + +type ResourceObject[T, ST any] interface { + comparable + client.Object + DeepCopy() T + GetObjectMeta() metav1.Object +} + +type ObjectStatusGetter[T, ST any] func(obj T) ST + +type ObjectFactory[T any] func() T + +type Resource[T ResourceObject[T, ST], ST any] struct { + name types.NamespacedName + currentObj T + changedObj T + emptyObj T + + objFactory ObjectFactory[T] + objStatusGetter ObjectStatusGetter[T, ST] + client client.Client +} + +func NewResource[T ResourceObject[T, ST], ST any](name types.NamespacedName, client client.Client, objFactory ObjectFactory[T], objStatusGetter ObjectStatusGetter[T, ST]) *Resource[T, ST] { + return &Resource[T, ST]{ + name: name, + client: client, + objFactory: objFactory, + objStatusGetter: objStatusGetter, + } +} + +func (r *Resource[T, ST]) getObjStatus(obj T) (ret ST) { + if obj != r.emptyObj { + ret = r.objStatusGetter(obj) + } + return +} + +func (r *Resource[T, ST]) Name() types.NamespacedName { + return r.name +} + +func (r *Resource[T, ST]) Fetch(ctx context.Context) error { + currentObj, err := helper.FetchObject(ctx, r.name, r.client, r.objFactory()) + if err != nil { + return err + } + + r.currentObj = currentObj + if r.IsEmpty() { + r.changedObj = r.emptyObj + return nil + } + + r.changedObj = currentObj.DeepCopy() + return nil +} + +func (r *Resource[T, ST]) IsEmpty() bool { + return r.currentObj == r.emptyObj +} + +func (r *Resource[T, ST]) Current() T { + return r.currentObj +} + +func (r *Resource[T, ST]) Changed() T { + return r.changedObj +} + +func (r *Resource[T, ST]) Update(ctx context.Context) error { + if r.IsEmpty() { + return nil + } + + finalizers := r.changedObj.GetFinalizers() + + if !reflect.DeepEqual(r.getObjStatus(r.currentObj), r.getObjStatus(r.changedObj)) { + slog.Info("Update Status") + if err := r.client.Status().Update(ctx, r.changedObj); err != nil { + return fmt.Errorf("error updating status subresource: %w", err) + } + r.changedObj.SetFinalizers(finalizers) + } + + if !slices.Equal(r.currentObj.GetFinalizers(), r.changedObj.GetFinalizers()) { + slog.Info("Patch Finalizers") + + patch, err := GetPatchFinalizers(r.changedObj.GetFinalizers()) + if err != nil { + return err + } + + if err = r.client.Patch(ctx, r.changedObj, patch); err != nil { + return fmt.Errorf("error updating: %w", err) + } + } + + return nil +} + +func GetPatchOwnerReferences(ownerReferences []metav1.OwnerReference) (client.Patch, error) { + data, err := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "ownerReferences": ownerReferences, + }, + }) + if err != nil { + return nil, err + } + + return client.RawPatch(types.MergePatchType, data), nil +} + +func GetPatchFinalizers(finalizers []string) (client.Patch, error) { + data, err := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": finalizers, + }, + }) + if err != nil { + return nil, err + } + + return client.RawPatch(types.MergePatchType, data), nil +} diff --git a/images/virtualization-artifact/pkg/controller/service/stat_service.go b/images/virtualization-artifact/pkg/controller/service/stat_service.go new file mode 100644 index 000000000..369bff766 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/stat_service.go @@ -0,0 +1,251 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "errors" + "fmt" + "log/slog" + "strconv" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/deckhouse/virtualization-controller/pkg/common" + "github.com/deckhouse/virtualization-controller/pkg/controller/monitoring" + "github.com/deckhouse/virtualization-controller/pkg/imageformat" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type StatService struct { + logger *slog.Logger +} + +func NewStatService() *StatService { + return &StatService{ + logger: slog.Default().With("svc", "stat"), + } +} + +func (s StatService) GetFormat(pod *corev1.Pod) string { + finalReport, err := monitoring.GetFinalReportFromPod(pod) + if err != nil { + s.logger.Error("GetFormat: Cannot get final report from pod", "err", err) + return "" + } + + if finalReport == nil { + return "" + } + + return finalReport.Format +} + +func (s StatService) GetCDROM(pod *corev1.Pod) bool { + finalReport, err := monitoring.GetFinalReportFromPod(pod) + if err != nil { + s.logger.Error("GetCDROM: Cannot get final report from pod", "err", err) + return false + } + + if finalReport == nil { + return false + } + + return imageformat.IsISO(finalReport.Format) +} + +func (s StatService) GetSize(pod *corev1.Pod) virtv2.ImageStatusSize { + finalReport, err := monitoring.GetFinalReportFromPod(pod) + if err != nil { + s.logger.Error("GetSize: Cannot get final report from pod", "err", err) + return virtv2.ImageStatusSize{} + } + + if finalReport == nil { + return virtv2.ImageStatusSize{} + } + + return virtv2.ImageStatusSize{ + Stored: finalReport.StoredSize(), + StoredBytes: strconv.FormatUint(finalReport.StoredSizeBytes, 10), + Unpacked: finalReport.UnpackedSize(), + UnpackedBytes: strconv.FormatUint(finalReport.UnpackedSizeBytes, 10), + } +} + +var ( + ErrNotInitialized = errors.New("not initialized") + ErrNotScheduled = errors.New("not scheduled") + ErrProvisioningFailed = errors.New("provisioning failed") +) + +func (s StatService) CheckPod(pod *corev1.Pod) error { + podInitializedCond, ok := getPodCondition(corev1.PodInitialized, pod.Status.Conditions) + if !ok || podInitializedCond.Status == corev1.ConditionFalse { + return fmt.Errorf("provisioning Pod %s/%s is %w: %s", pod.Namespace, pod.Name, ErrNotInitialized, podInitializedCond.Message) + } + + podScheduledCond, ok := getPodCondition(corev1.PodScheduled, pod.Status.Conditions) + if !ok || podScheduledCond.Status == corev1.ConditionFalse { + return fmt.Errorf("provisioning Pod %s/%s is %w: %s", pod.Namespace, pod.Name, ErrNotScheduled, podScheduledCond.Message) + } + + report, err := monitoring.GetFinalReportFromPod(pod) + if err != nil && !errors.Is(err, monitoring.ErrTerminationMessageNotFound) { + return err + } + + if report != nil && report.ErrMessage != "" { + return fmt.Errorf("%w: Pod %s/%s termination message: %s", ErrProvisioningFailed, pod.Namespace, pod.Name, report.ErrMessage) + } + + if pod.Status.Phase == corev1.PodFailed { + return fmt.Errorf("%w: Pod %s/%s failed", ErrProvisioningFailed, pod.Namespace, pod.Name) + } + + return nil +} + +func (s StatService) GetReasonError(pod *corev1.Pod) (string, string, error) { + report, err := monitoring.GetFinalReportFromPod(pod) + if err != nil { + s.logger.Error("GetError: Cannot get final report from pod", "err", err) + return "", "", err + } + + if report == nil { + return "", "", nil + } + + return "", report.ErrMessage, nil +} + +func (s StatService) GetDownloadSpeed(ownerUID types.UID, pod *corev1.Pod) virtv2.ImageStatusSpeed { + progress, err := monitoring.GetImportProgressFromPod(string(ownerUID), pod) + if err != nil { + s.logger.Error("GetDownloadSpeed: Cannot get import progress from pod", "err", err) + return virtv2.ImageStatusSpeed{} + } + + if progress == nil { + return virtv2.ImageStatusSpeed{} + } + + // TODO from final message get avg speed. + speed := virtv2.ImageStatusSpeed{ + Avg: progress.AvgSpeed(), + AvgBytes: strconv.FormatUint(progress.AvgSpeedRaw(), 10), + } + + if pod.Status.Phase != corev1.PodSucceeded { + speed.Current = progress.CurSpeed() + speed.CurrentBytes = strconv.FormatUint(progress.CurSpeedRaw(), 10) + } + + return speed +} + +type GetProgressOption interface { + Apply(progress string) string +} + +func NewScaleOption(low, high float64) *ScaleOption { + return &ScaleOption{ + Low: low, + High: high, + } +} + +type ScaleOption struct { + Low float64 + High float64 +} + +func (o ScaleOption) Apply(progress string) string { + return common.ScalePercentage(progress, o.Low, o.High) +} + +func (s StatService) GetProgress(ownerUID types.UID, pod *corev1.Pod, prevProgress string, opts ...GetProgressOption) string { + if pod == nil { + return prevProgress + } + + if pod.Status.Phase == corev1.PodSucceeded { + report, err := monitoring.GetFinalReportFromPod(pod) + if err != nil { + s.logger.Error("GetProgress: Cannot get final report from pod", "err", err) + return prevProgress + } + + if report.ErrMessage != "" { + return prevProgress + } + + res := "100%" + for _, o := range opts { + res = o.Apply(res) + } + + return res + } + + progress, err := monitoring.GetImportProgressFromPod(string(ownerUID), pod) + if err != nil { + s.logger.Error("GetProgress: Cannot get import progress from pod", "err", err) + return prevProgress + } + + if progress == nil { + return prevProgress + } + + res := progress.Progress() + for _, o := range opts { + res = o.Apply(res) + } + + return res +} + +func (s StatService) IsImportStarted(ownerUID types.UID, pod *corev1.Pod) bool { + progress, err := monitoring.GetImportProgressFromPod(string(ownerUID), pod) + if err != nil { + s.logger.Error("IsImportStarted: Cannot get import progress from pod", "err", err) + return false + } + + if progress == nil { + return false + } + + return progress.ProgressRaw() > 0 +} + +func (s StatService) IsUploadStarted(ownerUID types.UID, pod *corev1.Pod) bool { + return s.IsImportStarted(ownerUID, pod) +} + +func getPodCondition(condType corev1.PodConditionType, conds []corev1.PodCondition) (corev1.PodCondition, bool) { + for _, cond := range conds { + if cond.Type == condType { + return cond, true + } + } + + return corev1.PodCondition{}, false +} diff --git a/images/virtualization-artifact/pkg/controller/service/uploader_service.go b/images/virtualization-artifact/pkg/controller/service/uploader_service.go new file mode 100644 index 000000000..fc9117e09 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/uploader_service.go @@ -0,0 +1,222 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + 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/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" +) + +type UploaderService struct { + dvcrSettings *dvcr.Settings + client client.Client + image string + pullPolicy string + verbose string + controllerName string + protection *ProtectionService +} + +func NewUploaderService( + dvcrSettings *dvcr.Settings, + client client.Client, + image string, + pullPolicy string, + verbose string, + controllerName string, + protection *ProtectionService, +) *UploaderService { + return &UploaderService{ + dvcrSettings: dvcrSettings, + client: client, + image: image, + pullPolicy: pullPolicy, + verbose: verbose, + controllerName: controllerName, + protection: protection, + } +} + +func (s UploaderService) Start(ctx context.Context, settings *uploader.Settings, obj ObjectKind, sup *supplements.Generator, caBundle *datasource.CABundle) error { + ownerRef := metav1.NewControllerRef(obj, obj.GroupVersionKind()) + settings.Verbose = s.verbose + + pod, err := uploader.NewPod(s.getPodSettings(ownerRef, sup), settings).Create(ctx, s.client) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + err = supplements.EnsureForPod(ctx, s.client, sup, pod, caBundle, s.dvcrSettings) + if err != nil { + return err + } + + _, err = uploader.NewService(s.getServiceSettings(ownerRef, sup)).Create(ctx, s.client) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + ing, err := uploader.NewIngress(s.getIngressSettings(ownerRef, sup)).Create(ctx, s.client) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return supplements.EnsureForIngress(ctx, s.client, sup, ing, s.dvcrSettings) +} + +func (s UploaderService) CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) { + return s.CleanUpSupplements(ctx, sup) +} + +func (s UploaderService) CleanUpSupplements(ctx context.Context, sup *supplements.Generator) (bool, error) { + pod, err := s.GetPod(ctx, sup) + if err != nil { + return false, err + } + svc, err := s.GetService(ctx, sup) + if err != nil { + return false, err + } + ing, err := s.GetIngress(ctx, sup) + if err != nil { + return false, err + } + + err = s.protection.RemoveProtection(ctx, pod, svc, ing) + if err != nil { + return false, err + } + + var haveDeleted bool + + if pod != nil { + haveDeleted = true + err = s.client.Delete(ctx, pod) + if err != nil && !k8serrors.IsNotFound(err) { + return false, err + } + } + + if svc != nil { + haveDeleted = true + err = s.client.Delete(ctx, svc) + if err != nil && !k8serrors.IsNotFound(err) { + return false, err + } + } + + if ing != nil { + haveDeleted = true + err = s.client.Delete(ctx, ing) + if err != nil && !k8serrors.IsNotFound(err) { + return false, err + } + } + + return haveDeleted, nil +} + +func (s UploaderService) Protect(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { + return s.protection.AddProtection(ctx, pod, svc, ing) +} + +func (s UploaderService) Unprotect(ctx context.Context, pod *corev1.Pod, svc *corev1.Service, ing *netv1.Ingress) error { + return s.protection.RemoveProtection(ctx, pod, svc, ing) +} + +func (s UploaderService) GetPod(ctx context.Context, sup *supplements.Generator) (*corev1.Pod, error) { + pod, err := uploader.FindPod(ctx, s.client, sup) + if err != nil { + return nil, err + } + + return pod, nil +} + +func (s UploaderService) GetService(ctx context.Context, sup *supplements.Generator) (*corev1.Service, error) { + svc, err := uploader.FindService(ctx, s.client, sup) + if err != nil { + return nil, err + } + + return svc, nil +} + +func (s UploaderService) GetIngress(ctx context.Context, sup *supplements.Generator) (*netv1.Ingress, error) { + ing, err := uploader.FindIngress(ctx, s.client, sup) + if err != nil { + return nil, err + } + + return ing, nil +} + +func (s UploaderService) getPodSettings(ownerRef *metav1.OwnerReference, sup *supplements.Generator) *uploader.PodSettings { + uploaderPod := sup.UploaderPod() + uploaderSvc := sup.UploaderService() + return &uploader.PodSettings{ + Name: uploaderPod.Name, + Image: s.image, + PullPolicy: s.pullPolicy, + Namespace: uploaderPod.Namespace, + OwnerReference: *ownerRef, + ControllerName: s.controllerName, + InstallerLabels: map[string]string{}, + ServiceName: uploaderSvc.Name, + } +} + +func (s UploaderService) getServiceSettings(ownerRef *metav1.OwnerReference, sup *supplements.Generator) *uploader.ServiceSettings { + uploaderSvc := sup.UploaderService() + return &uploader.ServiceSettings{ + Name: uploaderSvc.Name, + Namespace: uploaderSvc.Namespace, + OwnerReference: *ownerRef, + } +} + +func (s UploaderService) getIngressSettings(ownerRef *metav1.OwnerReference, sup *supplements.Generator) *uploader.IngressSettings { + uploaderIng := sup.UploaderIngress() + uploaderSvc := sup.UploaderService() + secretName := s.dvcrSettings.UploaderIngressSettings.TLSSecret + if supplements.ShouldCopyUploaderTLSSecret(s.dvcrSettings, sup) { + secretName = sup.UploaderTLSSecretForIngress().Name + } + var class *string + if c := s.dvcrSettings.UploaderIngressSettings.Class; c != "" { + class = &c + } + return &uploader.IngressSettings{ + Name: uploaderIng.Name, + Namespace: uploaderIng.Namespace, + Host: s.dvcrSettings.UploaderIngressSettings.Host, + TLSSecretName: secretName, + ServiceName: uploaderSvc.Name, + ClassName: class, + OwnerReference: *ownerRef, + } +} diff --git a/images/virtualization-artifact/pkg/controller/supplements/generator.go b/images/virtualization-artifact/pkg/controller/supplements/generator.go index 2f715ba47..6bf79b19e 100644 --- a/images/virtualization-artifact/pkg/controller/supplements/generator.go +++ b/images/virtualization-artifact/pkg/controller/supplements/generator.go @@ -32,6 +32,15 @@ type Generator struct { UID types.UID } +func NewGenerator(prefix, name, namespace string, uid types.UID) *Generator { + return &Generator{ + Prefix: prefix, + Name: name, + Namespace: namespace, + UID: uid, + } +} + // DVCRAuthSecret returns name and namespace for auth Secret copy. func (g *Generator) DVCRAuthSecret() types.NamespacedName { name := fmt.Sprintf("%s-dvcr-auth-%s", g.Prefix, g.Name) @@ -100,6 +109,10 @@ func (g *Generator) DataVolume() types.NamespacedName { return g.shortenNamespaced(dvName) } +func (g *Generator) PersistentVolumeClaim() types.NamespacedName { + return g.DataVolume() +} + func (g *Generator) shortenNamespaced(name string) types.NamespacedName { return types.NamespacedName{ Name: strings.ShortenString(name, kvalidation.DNS1123SubdomainMaxLength), diff --git a/images/virtualization-artifact/pkg/controller/uploader/uploader_ingress.go b/images/virtualization-artifact/pkg/controller/uploader/uploader_ingress.go index a86ed0410..cd101c5fe 100644 --- a/images/virtualization-artifact/pkg/controller/uploader/uploader_ingress.go +++ b/images/virtualization-artifact/pkg/controller/uploader/uploader_ingress.go @@ -64,6 +64,10 @@ func (i *Ingress) Create(ctx context.Context, client client.Client) (*netv1.Ingr } func CleanupIngress(ctx context.Context, client client.Client, ing *netv1.Ingress) error { + if ing == nil { + return nil + } + return helper.CleanupObject(ctx, client, ing) } diff --git a/images/virtualization-artifact/pkg/controller/vm_reconciler_state_test.go b/images/virtualization-artifact/pkg/controller/vm_reconciler_state_test.go index 85014ed55..a25738e17 100644 --- a/images/virtualization-artifact/pkg/controller/vm_reconciler_state_test.go +++ b/images/virtualization-artifact/pkg/controller/vm_reconciler_state_test.go @@ -34,6 +34,8 @@ import ( "github.com/deckhouse/virtualization/api/core/v1alpha2" ) +var vmControllerLog = logf.Log.WithName("vm-controller-test") + func TestUnmarshalVMStatus(t *testing.T) { vmName := types.NamespacedName{ Namespace: "test-ns", diff --git a/images/virtualization-artifact/pkg/controller/vmd_controller.go b/images/virtualization-artifact/pkg/controller/vmd_controller.go deleted file mode 100644 index 7e1ef71f6..000000000 --- a/images/virtualization-artifact/pkg/controller/vmd_controller.go +++ /dev/null @@ -1,105 +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/builder" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/metrics" - - "github.com/deckhouse/virtualization-controller/pkg/dvcr" - diskmetrics "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/virtualdisk" - "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/two_phase_reconciler" - "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -const ( - vmdControllerName = "vd-controller" - vmdShortName = "vd" -) - -func NewVMDController( - ctx context.Context, - mgr manager.Manager, - log logr.Logger, - importerImage string, - uploaderImage string, - dvcrSettings *dvcr.Settings, -) (controller.Controller, error) { - reconciler := NewVMDReconciler( - importerImage, - uploaderImage, - ImporterPodVerbose, - ImporterPodPullPolicy, - dvcrSettings, - ) - mgrCache := mgr.GetCache() - reconcilerCore := two_phase_reconciler.NewReconcilerCore[*VMDReconcilerState]( - reconciler, - NewVMDReconcilerState, - two_phase_reconciler.ReconcilerOptions{ - Client: mgr.GetClient(), - Cache: mgrCache, - Recorder: mgr.GetEventRecorderFor(vmdControllerName), - Scheme: mgr.GetScheme(), - Log: log.WithName(vmdControllerName), - }) - - c, err := controller.New(vmdControllerName, 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 - } - - if err = builder.WebhookManagedBy(mgr). - For(&v1alpha2.VirtualDisk{}). - WithValidator(NewVMDValidator(log)). - Complete(); err != nil { - return nil, err - } - - diskmetrics.SetupCollector(&diskLister{diskCache: mgrCache}, metrics.Registry) - - log.Info("Initialized VirtualDisk controller") - return c, nil -} - -type diskLister struct { - diskCache cache.Cache -} - -func (l diskLister) List() ([]v1alpha2.VirtualDisk, error) { - disks := v1alpha2.VirtualDiskList{} - err := l.diskCache.List(context.Background(), &disks) - if err != nil { - return nil, err - } - return disks.Items, nil -} diff --git a/images/virtualization-artifact/pkg/controller/vmd_datavolume.go b/images/virtualization-artifact/pkg/controller/vmd_datavolume.go deleted file mode 100644 index 01bee3e4d..000000000 --- a/images/virtualization-artifact/pkg/controller/vmd_datavolume.go +++ /dev/null @@ -1,178 +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" - - corev1 "k8s.io/api/core/v1" - "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" - vmdutil "github.com/deckhouse/virtualization-controller/pkg/common/vmd" - "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" -) - -var ( - ErrDataSourceNotReady = errors.New("data source is not ready") - ErrPVCSizeSmallerImageVirtualSize = errors.New("persistentVolumeClaim size is smaller than image virtual size") -) - -func (r *VMDReconciler) getPVCSize(vmd *virtv2.VirtualDisk, state *VMDReconcilerState, opts two_phase_reconciler.ReconcilerOptions) (resource.Quantity, error) { - pvcSize := vmd.Spec.PersistentVolumeClaim.Size - - if vmdutil.IsBlankPVC(vmd) { - if pvcSize == nil || pvcSize.IsZero() { - return resource.Quantity{}, errors.New("spec.persistentVolumeClaim.size should be set for blank virtual disk") - } - - return *pvcSize, nil - } - - var unpackedSize resource.Quantity - - switch { - case vmdutil.IsTwoPhaseImport(vmd): - // 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 vmdutil.IsDVCRSource(vmd): - 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") - } - - if pvcSize != nil && !pvcSize.IsZero() && pvcSize.Cmp(unpackedSize) == -1 { - opts.Recorder.Event(state.VMD.Current(), corev1.EventTypeWarning, virtv2.ReasonErrWrongPVCSize, ErrPVCSizeSmallerImageVirtualSize.Error()) - - return resource.Quantity{}, ErrPVCSizeSmallerImageVirtualSize - } - - // Adjust PVC size to feat image onto scratch PVC. - // TODO(future): remove size adjusting after get rid of scratch. - adjustedSize := dvutil.AdjustPVCSize(unpackedSize) - - if pvcSize != nil && pvcSize.Cmp(adjustedSize) == 1 { - return *pvcSize, nil - } - - return adjustedSize, nil -} - -// createDataVolume creates DataVolume resource to copy image from DVCR to PVC. -func (r *VMDReconciler) createDataVolume(ctx context.Context, vmd *virtv2.VirtualDisk, state *VMDReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - // Retrieve PVC size. - pvcSize, err := r.getPVCSize(vmd, state, opts) - if err != nil { - return fmt.Errorf("failed to get pvc size: %w", err) - } - - dv, err := r.makeDataVolumeFromVMD(state, state.Supplements.DataVolume(), pvcSize) - if err != nil { - return fmt.Errorf("apply virtual disk spec to DataVolume: %w", err) - } - - opts.Log.V(2).Info(fmt.Sprintf("DV gvk before Create: %s", dv.GetObjectKind().GroupVersionKind().String())) - - 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 VD/%s: %w", dv.GetName(), vmd.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, "dv.gvk", dv.GetObjectKind().GroupVersionKind()) - - if vmdutil.IsTwoPhaseImport(vmd) || vmdutil.IsDVCRSource(vmd) { - // Copy auth credentials and ca bundle to access DVCR as 'registry' data source. - // Set DV as an ownerRef to auto-cleanup these copies on DV deletion. - 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 *VMDReconciler) makeDataVolumeFromVMD(state *VMDReconcilerState, dvName types.NamespacedName, pvcSize resource.Quantity) (*cdiv1.DataVolume, error) { - dvBuilder := kvbuilder.NewDV(dvName) - vmd := state.VMD.Current() - ds := vmd.Spec.DataSource - - authSecretName := state.Supplements.DVCRAuthSecretForDV().Name - caBundleName := state.Supplements.DVCRCABundleConfigMapForDV().Name - - // Set datasource: - // 'registry' if import is two phased. - // 'blank' if vmd has no datasource. - // TODO(refactor) Remove switch if there are only 2 options for the DataVolume source: DVCR and blank. - switch { - case vmdutil.IsTwoPhaseImport(vmd): - // 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.RegistryImageForVMD(vmd.Name, vmd.Namespace) - dvBuilder.SetRegistryDataSource(dvcrSourceImageName, authSecretName, caBundleName) - case ds != nil && ds.Type == virtv2.DataSourceTypeObjectRef: - if ds.ObjectRef == nil { - return nil, fmt.Errorf("nil objectRef %q", vmdutil.GetDataSourceType(vmd)) - } - - switch ds.ObjectRef.Kind { - case virtv2.VirtualDiskObjectRefKindVirtualImage: - dvcrSourceImageName := r.dvcrSettings.RegistryImageForVMI(ds.ObjectRef.Name, vmd.Namespace) - dvBuilder.SetRegistryDataSource(dvcrSourceImageName, authSecretName, caBundleName) - case virtv2.VirtualDiskObjectRefKindClusterVirtualImage: - 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) - } - case vmdutil.IsBlankPVC(vmd): - dvBuilder.SetBlankDataSource() - default: - return nil, fmt.Errorf("unsupported dataSource type %q", vmdutil.GetDataSourceType(vmd)) - } - - dvBuilder.SetPVC(vmd.Spec.PersistentVolumeClaim.StorageClass, pvcSize) - - dvBuilder.SetOwnerRef(vmd, vmd.GetObjectKind().GroupVersionKind()) - dvBuilder.AddFinalizer(virtv2.FinalizerDVProtection) - - return dvBuilder.GetResource(), nil -} diff --git a/images/virtualization-artifact/pkg/controller/vmd_importer.go b/images/virtualization-artifact/pkg/controller/vmd_importer.go deleted file mode 100644 index 1a14b4222..000000000 --- a/images/virtualization-artifact/pkg/controller/vmd_importer.go +++ /dev/null @@ -1,125 +0,0 @@ -/* -Copyright 2024 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "context" - "errors" - "fmt" - - "github.com/deckhouse/virtualization-controller/pkg/common/datasource" - vmdutil "github.com/deckhouse/virtualization-controller/pkg/common/vmd" - 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 *VMDReconciler) startImporterPod(ctx context.Context, state *VMDReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - vmd := state.VMD.Current() - if vmd.Spec.DataSource == nil { - opts.Log.Error(errors.New("start importer Pod for empty dataSource"), "Possible bug") - return nil - } - - opts.Log.V(1).Info("Creating importer POD for VD", "vd.Name", vmd.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, vmd, opts.Recorder, opts.Client) - if err != nil { - return err - } - } - - opts.Log.V(1).Info("Created importer POD", "pod.Name", pod.Name) - - return supplements.EnsureForPod(ctx, opts.Client, state.Supplements, pod, datasource.NewCABundleForVMD(vmd.Spec.DataSource), r.dvcrSettings) -} - -// createImporterSettings fills settings for the dvcr-importer binary. -func (r *VMDReconciler) createImporterSettings(state *VMDReconcilerState) (*importer.Settings, error) { - vmd := state.VMD.Current() - - settings := &importer.Settings{ - Verbose: r.verbose, - } - - ds := vmd.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.VirtualDiskObjectRefKindVirtualImage: - // Note: use namespace from the current VMD resource. - dvcrSourceImageName := r.dvcrSettings.RegistryImageForVMI(ds.ObjectRef.Name, vmd.Namespace) - importer.ApplyDVCRSourceSettings(settings, dvcrSourceImageName) - case virtv2alpha1.VirtualDiskObjectRefKindClusterVirtualImage: - 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.RegistryImageForVMD(vmd.Name, vmd.Namespace) - importer.ApplyDVCRDestinationSettings(settings, r.dvcrSettings, state.Supplements, dvcrDestImageName) - - // TODO Update proxy settings. - - return settings, nil -} - -func (r *VMDReconciler) createImporterPodSettings(state *VMDReconcilerState) *importer.PodSettings { - importerPod := state.Supplements.ImporterPod() - return &importer.PodSettings{ - Name: importerPod.Name, - Image: r.importerImage, - PullPolicy: r.pullPolicy, - Namespace: importerPod.Namespace, - OwnerReference: vmdutil.MakeOwnerReference(state.VMD.Current()), - ControllerName: vmdControllerName, - InstallerLabels: map[string]string{}, - } -} diff --git a/images/virtualization-artifact/pkg/controller/vmd_reconciler.go b/images/virtualization-artifact/pkg/controller/vmd_reconciler.go deleted file mode 100644 index f76355fd7..000000000 --- a/images/virtualization-artifact/pkg/controller/vmd_reconciler.go +++ /dev/null @@ -1,717 +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" - "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" - "github.com/deckhouse/virtualization-controller/pkg/common/vmd" - 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/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 VMDReconciler struct { - *vmattachee.AttacheeReconciler[*virtv2.VirtualDisk, virtv2.VirtualDiskStatus] - - importerImage string - uploaderImage string - verbose string - pullPolicy string - dvcrSettings *dvcr.Settings -} - -func NewVMDReconciler(importerImage, uploaderImage, verbose, pullPolicy string, dvcrSettings *dvcr.Settings) *VMDReconciler { - return &VMDReconciler{ - importerImage: importerImage, - uploaderImage: uploaderImage, - verbose: verbose, - pullPolicy: pullPolicy, - dvcrSettings: dvcrSettings, - AttacheeReconciler: vmattachee.NewAttacheeReconciler[ - *virtv2.VirtualDisk, - virtv2.VirtualDiskStatus, - ](), - } -} - -func (r *VMDReconciler) SetupController(ctx context.Context, mgr manager.Manager, ctr controller.Controller) error { - if err := ctr.Watch(source.Kind(mgr.GetCache(), &virtv2.VirtualDisk{}), &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 virtual disk: %w", err) - } - - if err := ctr.Watch( - source.Kind(mgr.GetCache(), &cdiv1.DataVolume{}), - handler.EnqueueRequestForOwner( - mgr.GetScheme(), - mgr.GetRESTMapper(), - &virtv2.VirtualDisk{}, - 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.VirtualDisk{}, - 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 Pod and creates a DataVolume to import image into PVC. -func (r *VMDReconciler) Sync(ctx context.Context, _ reconcile.Request, state *VMDReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - log := opts.Log.WithValues("vd.name", state.VMD.Current().GetName()) - - log.V(2).Info("Sync VD") - - if r.AttacheeReconciler.Sync(ctx, state.AttacheeState, opts) { - return nil - } - - switch { - case state.IsDeletion(): - log.V(1).Info("Delete VD, remove protective finalizers") - return r.cleanupOnDeletion(ctx, state, opts) - case !state.IsProtected(): - // Set protective finalizer atomically. - if controllerutil.AddFinalizer(state.VMD.Changed(), virtv2.FinalizerVMDCleanup) { - state.SetReconcilerResult(&reconcile.Result{Requeue: true}) - return nil - } - case state.IsLost(): - return nil - case state.IsReady(): - opts.Log.Info("virtual disk ready: cleanup underlying resources") - // Delete underlying importer/uploader Pod, Service and DataVolume and stop the reconcile process. - if cc.ShouldCleanupSubResources(state.VMD.Current()) { - if err := r.cleanup(ctx, state.VMD.Changed(), state.Client, state); err != nil { - return err - } - } - - if state.PVC == nil { - opts.Log.Info("PVC lost") - return nil - } - - oldSize := state.PVC.Spec.Resources.Requests[corev1.ResourceStorage] - newSize := state.VMD.Current().Spec.PersistentVolumeClaim.Size - - if newSize == nil || newSize.Cmp(oldSize) == -1 { - return nil - } - - if !newSize.Equal(oldSize) { - opts.Log.Info("Increase PVC size", "oldPVCSize", oldSize.String(), "newPVCSize", newSize.String()) - - state.PVC.Spec.Resources.Requests[corev1.ResourceStorage] = *newSize - - err := opts.Client.Update(ctx, state.PVC) - if err != nil { - return fmt.Errorf("failed to increase pvc size: %w", err) - } - } - - if !newSize.Equal(state.PVC.Status.Capacity[corev1.ResourceStorage]) { - opts.Log.Info("PVC is in a process of increasing: wait for the PVC to be increased") - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - } - - return nil - case state.IsFailed(): - opts.Log.Info("virtual disk failed: cleanup underlying resources") - // Delete underlying importer/uploader Pod, Service and DataVolume and stop the reconcile process. - if cc.ShouldCleanupSubResources(state.VMD.Current()) { - if err := r.cleanup(ctx, state.VMD.Changed(), state.Client, state); err != nil { - return err - } - } - - return nil - // First phase: import to DVCR. - case state.ShouldTrackPod() && !state.IsPodComplete(): - // Start and track importer/uploader Pod. - switch { - case state.CanStartPod(): - // Create Pod using name and namespace from annotation. - log.V(1).Info("Start new Pod for virtual disk") - // Create importer/uploader pod, make sure the VMD 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. - log.V(2).Info("Requeue: wait until Pod is completed", "vd.name", state.VMD.Current().Name) - if err := r.ensurePodFinalizers(ctx, state, opts); err != nil { - return err - } - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - return nil - } - // Second phase: import to PVC. - case state.ShouldTrackDataVolume() && (!state.ShouldTrackPod() || state.IsPodComplete()): - // Start and track DataVolume. - switch { - case vmd.IsDVCRSource(state.VMD.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(): - if state.ShouldTrackPod() { - finalReport, err := monitoring.GetFinalReportFromPod(state.Pod) - if err != nil { - return err - } - - if finalReport.ErrMessage != "" { - log.V(1).Info("Got error in final report", "error", finalReport.ErrMessage) - return nil - } - } - - log.V(1).Info("Create DataVolume for VD") - - err := r.createDataVolume(ctx, state.VMD.Current(), state, opts) - if err != nil { - if errors.Is(err, ErrPVCSizeSmallerImageVirtualSize) { - return nil - } - if !errors.Is(err, ErrDataSourceNotReady) { - return err - } - - log.V(1).Info("Wait for the data source to be ready", "err", 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. - log.V(2).Info("Requeue: wait until DataVolume is completed", "vd.name", state.VMD.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("vd.Status.Phase='%s'", state.VMD.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.VMD.Current(), corev1.EventTypeWarning, virtv2.ReasonErrUnknownState, fmt.Sprintf("virtual disk has unexpected state, recreate it to start import again. %s", details)) - - return nil -} - -func (r *VMDReconciler) UpdateStatus(_ context.Context, _ reconcile.Request, state *VMDReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - log := opts.Log.WithValues("vd.name", state.VMD.Current().GetName()) - - log.V(2).Info("Update VD status") - - // 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.VMD.Current(), corev1.EventTypeWarning, virtv2.ReasonErrImportFailed, fmt.Sprintf("importer pod phase '%s', message '%s'", state.Pod.Status.Phase, state.Pod.Status.ContainerStatuses[0].LastTerminationState.Terminated.Message)) - } - } - - vmdStatus := state.VMD.Current().Status.DeepCopy() - if vmdStatus.Progress == "" { - vmdStatus.Progress = "0%" - } - - switch { - case vmdStatus.Phase == "": - vmdStatus.Phase = virtv2.DiskPending - state.SetReconcilerResult(&reconcile.Result{Requeue: true}) - case state.IsReady(): - if state.PVC == nil { - log.V(1).Info("PVC not found for ready vd") - vmdStatus.Phase = virtv2.DiskPVCLost - break - } - - if state.PVC.Status.Phase == corev1.ClaimBound { - vmdStatus.Capacity = util.GetPointer(state.PVC.Status.Capacity[corev1.ResourceStorage]).String() - } - case state.IsFailed(), state.IsLost(): - break - case state.ShouldTrackPod() && state.IsPodRunning(): - log.V(2).Info("Fetch progress from Pod") - - // Set statue UploadCommand if necessary. - if state.VMD.Current().Spec.DataSource.Type == virtv2.DataSourceTypeUpload && - vmdStatus.UploadCommand == "" && - state.Ingress != nil && - state.Ingress.GetAnnotations()[cc.AnnUploadURL] != "" { - vmdStatus.UploadCommand = fmt.Sprintf( - "curl %s -T example.iso", - state.Ingress.GetAnnotations()[cc.AnnUploadURL], - ) - } - - progress, err := monitoring.GetImportProgressFromPod(string(state.VMD.Current().GetUID()), state.Pod) - if err != nil { - opts.Recorder.Event(state.VMD.Current(), corev1.EventTypeWarning, virtv2.ReasonErrGetProgressFailed, "Error fetching progress metrics from Pod "+err.Error()) - return err - } - if progress != nil { - log.V(2).Info("Got Pod progress", "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) - } - vmdStatus.Progress = progressPct - vmdStatus.DownloadSpeed.Avg = progress.AvgSpeed() - vmdStatus.DownloadSpeed.AvgBytes = strconv.FormatUint(progress.AvgSpeedRaw(), 10) - vmdStatus.DownloadSpeed.Current = progress.CurSpeed() - vmdStatus.DownloadSpeed.CurrentBytes = strconv.FormatUint(progress.CurSpeedRaw(), 10) - } - - // Set VMD phase. - if state.VMD.Current().Spec.DataSource.Type == virtv2.DataSourceTypeUpload && (progress == nil || progress.ProgressRaw() == 0) { - vmdStatus.Phase = virtv2.DiskWaitForUserUpload - // Fail if uploading time has expired. - if helper.GetAge(state.Pod) > cc.UploaderWaitDuration { - vmdStatus.Phase = virtv2.DiskFailed - vmdStatus.FailureReason = virtv2.ReasonErrUploaderWaitDurationExpired - vmdStatus.FailureMessage = "uploading time expired" - } - } else { - vmdStatus.Phase = virtv2.DiskProvisioning - } - case state.ShouldTrackDataVolume() && state.CanCreateDataVolume(): - _, err := r.getPVCSize(state.VMD.Current(), state, opts) - if err != nil && errors.Is(err, ErrPVCSizeSmallerImageVirtualSize) { - vmdStatus.Phase = virtv2.DiskFailed - vmdStatus.FailureReason = virtv2.ReasonErrWrongPVCSize - vmdStatus.FailureMessage = ErrPVCSizeSmallerImageVirtualSize.Error() - break - } - if state.ShouldTrackPod() { - finalReport, err := monitoring.GetFinalReportFromPod(state.Pod) - if err != nil { - return err - } - - if finalReport.ErrMessage != "" { - vmdStatus.Phase = virtv2.DiskFailed - vmdStatus.FailureReason = virtv2.ReasonErrImportFailed - vmdStatus.FailureMessage = finalReport.ErrMessage - opts.Recorder.Event(state.VMD.Current(), corev1.EventTypeWarning, virtv2.ReasonErrImportFailed, finalReport.ErrMessage) - break - } - - vmdStatus.DownloadSpeed.Avg = finalReport.GetAverageSpeed() - vmdStatus.DownloadSpeed.AvgBytes = strconv.FormatUint(finalReport.GetAverageSpeedRaw(), 10) - } - - vmdStatus.DownloadSpeed.Current = "" - vmdStatus.DownloadSpeed.CurrentBytes = "" - case state.ShouldTrackDataVolume() && state.IsDataVolumeInProgress(): - // Set phase from DataVolume resource. - vmdStatus.Phase = MapDataVolumePhaseToVMDPhase(state.DV.Status.Phase) - - // Download speed is not available from DataVolume. - vmdStatus.DownloadSpeed.Current = "" - vmdStatus.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 != "" { - vmdStatus.Progress = common.ScalePercentage(dvProgress, 50.0, 100.0) - } else { - vmdStatus.Progress = "50%" - } - - // Copy capacity from PVC. - if state.PVC != nil && state.PVC.Status.Phase == corev1.ClaimBound { - vmdStatus.Capacity = util.GetPointer(state.PVC.Status.Capacity[corev1.ResourceStorage]).String() - } - case state.ShouldTrackDataVolume() && state.IsDataVolumeComplete(): - if state.PVC == nil { - log.V(1).Info("PVC not found for completed vd") - vmdStatus.Phase = virtv2.DiskPVCLost - break - } - - if state.PVC.Status.Phase != corev1.ClaimBound { - log.V(1).Info("Wait for the PVC to enter the Bound phase") - state.SetReconcilerResult(&reconcile.Result{RequeueAfter: 2 * time.Second}) - break - } - - log.V(1).Info("Import completed successfully") - - vmdStatus.Phase = virtv2.DiskReady - vmdStatus.Progress = "100%" - - opts.Recorder.Event(state.VMD.Current(), corev1.EventTypeNormal, virtv2.ReasonImportSucceeded, "Successfully imported") - - // Cleanup download speed. - vmdStatus.DownloadSpeed.Current = "" - vmdStatus.DownloadSpeed.CurrentBytes = "" - - // PVC name is the same as the DataVolume name. - vmdStatus.Target.PersistentVolumeClaim = state.PVC.Name - - // Copy capacity from PVC if IsDataVolumeInProgress was very quick. - vmdStatus.Capacity = util.GetPointer(state.PVC.Status.Capacity[corev1.ResourceStorage]).String() - } - - state.VMD.Changed().Status = *vmdStatus - - return nil -} - -func MapDataVolumePhaseToVMDPhase(phase cdiv1.DataVolumePhase) virtv2.DiskPhase { - switch phase { - case cdiv1.PhaseUnset, cdiv1.Unknown, cdiv1.Pending: - return virtv2.DiskPending - 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.DiskProvisioning - case cdiv1.Succeeded: - return virtv2.DiskReady - case cdiv1.Failed: - return virtv2.DiskFailed - default: - return virtv2.DiskUnknown - } -} - -func (r *VMDReconciler) setPVCOwnerReference(ctx context.Context, state *VMDReconcilerState, apiClient client.Client) error { - if state.PVC == nil { - return nil - } - - state.PVC.OwnerReferences = []v1.OwnerReference{ - *v1.NewControllerRef(state.VMD.Current(), state.VMD.Current().GroupVersionKind()), - } - - err := apiClient.Update(ctx, state.PVC) - if err != nil { - return fmt.Errorf("failed to set pvc owner ref: %w", err) - } - - return nil -} - -// ensurePodFinalizers adds protective finalizers on importer/uploader Pod and Service dependencies. -func (r *VMDReconciler) ensurePodFinalizers(ctx context.Context, state *VMDReconcilerState, 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 *VMDReconciler) ensureDVFinalizers(ctx context.Context, state *VMDReconcilerState, 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 *VMDReconciler) ShouldDeleteChildResources(state *VMDReconcilerState) bool { - return state.Pod != nil || state.Service != nil || state.Ingress != nil || state.PV != nil || state.PVC != nil || state.DV != nil -} - -// removeFinalizerChildResources removes protective finalizers on Pod, Service, DataVolume, PersistentVolumeClaim and PersistentVolume dependencies. -func (r *VMDReconciler) removeFinalizerChildResources(ctx context.Context, state *VMDReconcilerState, 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 *VMDReconciler) startPod( - ctx context.Context, - state *VMDReconcilerState, - opts two_phase_reconciler.ReconcilerOptions, -) error { - vmd := state.VMD.Current() - - // Should not happen, but check it anyway. - if vmd.Spec.DataSource == nil { - return nil - } - - switch vmd.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 *VMDReconciler) cleanup(ctx context.Context, vmd *virtv2.VirtualDisk, client client.Client, state *VMDReconcilerState) error { - 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) - // } - } - - if vmd.Spec.DataSource == nil { - return nil - } - - switch vmd.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 *VMDReconciler) cleanupOnDeletion(ctx context.Context, state *VMDReconcilerState, 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.VMD.Current(), opts.Client, state); 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.VMD.Changed(), virtv2.FinalizerVMDCleanup) - return nil -} - -func (r *VMDReconciler) FilterAttachedVM(vm *virtv2.VirtualMachine) bool { - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind == virtv2.DiskDevice { - return true - } - } - - return false -} - -func (r *VMDReconciler) EnqueueFromAttachedVM(vm *virtv2.VirtualMachine) []reconcile.Request { - var requests []reconcile.Request - - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind != virtv2.DiskDevice { - 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/vmd_reconciler_state.go b/images/virtualization-artifact/pkg/controller/vmd_reconciler_state.go deleted file mode 100644 index c0d2b6676..000000000 --- a/images/virtualization-artifact/pkg/controller/vmd_reconciler_state.go +++ /dev/null @@ -1,280 +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" - - vmdutil "github.com/deckhouse/virtualization-controller/pkg/common/vmd" - 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 VMDReconcilerState struct { - *vmattachee.AttacheeState[*virtv2.VirtualDisk, virtv2.VirtualDiskStatus] - - Client client.Client - Supplements *supplements.Generator - Result *reconcile.Result - - VMD *helper.Resource[*virtv2.VirtualDisk, virtv2.VirtualDiskStatus] - DV *cdiv1.DataVolume - PVC *corev1.PersistentVolumeClaim - PV *corev1.PersistentVolume - Pod *corev1.Pod - Service *corev1.Service - Ingress *netv1.Ingress - DVCRDataSource *DVCRDataSource -} - -func NewVMDReconcilerState(name types.NamespacedName, log logr.Logger, client client.Client, cache cache.Cache) *VMDReconcilerState { - state := &VMDReconcilerState{ - Client: client, - VMD: helper.NewResource( - name, log, client, cache, - func() *virtv2.VirtualDisk { return &virtv2.VirtualDisk{} }, - func(obj *virtv2.VirtualDisk) virtv2.VirtualDiskStatus { return obj.Status }, - ), - } - - state.AttacheeState = vmattachee.NewAttacheeState( - state, - virtv2.FinalizerVMDProtection, - state.VMD, - ) - - return state -} - -func (state *VMDReconcilerState) ApplySync(ctx context.Context, _ logr.Logger) error { - if err := state.VMD.UpdateMeta(ctx); err != nil { - return fmt.Errorf("unable to update virtual disk %q meta: %w", state.VMD.Name(), err) - } - return nil -} - -func (state *VMDReconcilerState) ApplyUpdateStatus(ctx context.Context, _ logr.Logger) error { - return state.VMD.UpdateStatus(ctx) -} - -func (state *VMDReconcilerState) SetReconcilerResult(result *reconcile.Result) { - state.Result = result -} - -func (state *VMDReconcilerState) GetReconcilerResult() *reconcile.Result { - return state.Result -} - -func (state *VMDReconcilerState) Reload(ctx context.Context, req reconcile.Request, log logr.Logger, client client.Client) error { - err := state.VMD.Fetch(ctx) - if err != nil { - return fmt.Errorf("unable to get %q: %w", req.NamespacedName, err) - } - - if state.VMD.IsEmpty() { - log.Info("Reconcile observe an absent VD: it may be deleted", "vd.name", req.Name, "vd.namespace", req.Namespace) - return nil - } - - state.Supplements = &supplements.Generator{ - Prefix: vmdShortName, - Name: state.VMD.Current().Name, - Namespace: state.VMD.Current().Namespace, - UID: state.VMD.Current().UID, - } - - if state.VMD.Current().Spec.DataSource != nil { - switch state.VMD.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 = NewDVCRDataSourcesForVMD(ctx, state.VMD.Current().Spec.DataSource, state.VMD.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 *VMDReconcilerState) ShouldReconcile(log logr.Logger) bool { - if state.VMD.IsEmpty() { - return false - } - - if state.AttacheeState.ShouldReconcile(log) { - return true - } - - return true -} - -func (state *VMDReconcilerState) IsProtected() bool { - return controllerutil.ContainsFinalizer(state.VMD.Current(), virtv2.FinalizerVMDCleanup) -} - -func (state *VMDReconcilerState) IsLost() bool { - if state.VMD.IsEmpty() { - return false - } - return state.VMD.Current().Status.Phase == virtv2.DiskPVCLost -} - -func (state *VMDReconcilerState) IsReady() bool { - if state.VMD.IsEmpty() { - return false - } - return state.VMD.Current().Status.Phase == virtv2.DiskReady -} - -func (state *VMDReconcilerState) IsFailed() bool { - if state.VMD.IsEmpty() { - return false - } - return state.VMD.Current().Status.Phase == virtv2.DiskFailed -} - -func (state *VMDReconcilerState) IsDeletion() bool { - if state.VMD.IsEmpty() { - return false - } - return state.VMD.Current().DeletionTimestamp != nil -} - -func (state *VMDReconcilerState) ShouldTrackPod() bool { - if state.VMD.IsEmpty() { - return false - } - - if state.VMD.Current().Spec.DataSource == nil { - return false - } - - // Importer Pod is not needed if source image is already in DVCR. - if vmdutil.IsDVCRSource(state.VMD.Current()) { - return false - } - - // Use 2 phase import process for HTTP, Upload and ContainerImage sources. - return vmdutil.IsTwoPhaseImport(state.VMD.Current()) -} - -// CanStartPod returns whether importer Pod can be started. -// NOTE: valid only if ShouldTrackPod is true. -func (state *VMDReconcilerState) 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 *VMDReconcilerState) IsPodComplete() bool { - return state.Pod != nil && cc.IsPodComplete(state.Pod) -} - -// IsPodRunning returns whether importer/uploader Pod is in progress. -func (state *VMDReconcilerState) IsPodRunning() bool { - return state.Pod != nil && state.Pod.Status.Phase == corev1.PodRunning -} - -func (state *VMDReconcilerState) ShouldTrackDataVolume() bool { - return !state.VMD.IsEmpty() -} - -func (state *VMDReconcilerState) CanCreateDataVolume() bool { - return state.DV == nil && !state.IsReady() -} - -func (state *VMDReconcilerState) IsDataVolumeInProgress() bool { - return state.DV != nil && state.DV.Status.Phase != cdiv1.Succeeded -} - -func (state *VMDReconcilerState) IsDataVolumeComplete() bool { - return state.DV != nil && state.DV.Status.Phase == cdiv1.Succeeded -} - -func (state *VMDReconcilerState) IsAttachedToVM(vm virtv2.VirtualMachine) bool { - if state.VMD.IsEmpty() { - return false - } - - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind == virtv2.DiskDevice && bda.Name == state.VMD.Name().Name { - return true - } - } - - return false -} diff --git a/images/virtualization-artifact/pkg/controller/vmd_reconciler_test.go b/images/virtualization-artifact/pkg/controller/vmd_reconciler_test.go deleted file mode 100644 index 50fe2f876..000000000 --- a/images/virtualization-artifact/pkg/controller/vmd_reconciler_test.go +++ /dev/null @@ -1,208 +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_test - -// TODO write tests for VMD and VMI. -// var _ = Describe("VMD", func() { -// var reconciler *two_phase_reconciler.ReconcilerCore[*controller.VMDReconcilerState] -// var reconcileExecutor *testutil.ReconcileExecutor -// -// AfterEach(func() { -// if reconcileExecutor != nil { -// reconcileExecutor = nil -// } -// }) -// -// AfterEach(func() { -// if reconcileExecutor != nil && reconciler.Recorder != nil { -// close(reconciler.Recorder.(*record.FakeRecorder).Events) -// } -// }) -// -// It("Successfully imports image by HTTP source", func() { -// ctx := context.Background() -// -// var dvName string -// -// { -// vmd := &virtv2.VirtualDisk{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "test-vmd", -// Namespace: "test-ns", -// Labels: nil, -// Annotations: nil, -// }, -// Spec: virtv2.VirtualDiskSpec{ -// DataSource: &virtv2.VirtualDiskDataSource{ -// Type: virtv2.DataSourceTypeHTTP, -// HTTP: &virtv2.DataSourceHTTP{ -// URL: "http://mydomain.org/image.img", -// }, -// }, -// PersistentVolumeClaim: virtv2.VirtualDiskPersistentVolumeClaim{ -// Size: "10Gi", -// StorageClass: "local-path", -// }, -// }, -// } -// -// reconciler = controller.NewVMDReconciler(controller.TestReconcilerOptions{ -// KnownObjects: []client.Object{ -// &virtv2.VirtualMachine{}, -// &virtv2.VirtualDisk{}, -// &virtv2.ClusterVirtualImage{}, -// &cdiv1.DataVolume{}, -// }, -// RuntimeObjects: []runtime.Object{vmd}, -// }) -// reconcileExecutor = testutil.NewReconcileExecutor(types.NamespacedName{Name: "test-vmd", Namespace: "test-ns"}) -// } -// -// { -// err := reconcileExecutor.Execute(ctx, reconciler) -// Expect(err).NotTo(HaveOccurred()) -// -// vmd, err := helper.FetchObject(ctx, types.NamespacedName{Name: "test-vmd", Namespace: "test-ns"}, reconciler.Client, &virtv2.VirtualDisk{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(vmd).NotTo(BeNil()) -// Expect(vmd.Status.Phase).To(Equal(virtv2.DiskPending)) -// Expect(vmd.Status.Progress).To(Equal("N/A")) -// Expect(vmd.Status.Capacity).To(Equal("")) -// Expect(vmd.Status.Target.PersistentVolumeClaim).To(Equal("")) -// -// // UUID suffix -// Expect(strings.HasPrefix(vmd.Annotations[controller.AnnVMDDataVolume], "virtual-machine-disk-")).To(BeTrue(), fmt.Sprintf("unexpected DataVolume name %q", vmd.Annotations[controller.AnnVMDDataVolume])) -// Expect(len(vmd.Annotations[controller.AnnVMDDataVolume])).To(Equal(21 + 36)) -// } -// -// { -// err := reconcileExecutor.Execute(ctx, reconciler) -// Expect(err).NotTo(HaveOccurred()) -// -// vmd, err := helper.FetchObject(ctx, types.NamespacedName{Name: "test-vmd", Namespace: "test-ns"}, reconciler.Client, &virtv2.VirtualDisk{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(vmd).NotTo(BeNil()) -// Expect(vmd.Status.Phase).To(Equal(virtv2.DiskPending)) -// Expect(vmd.Status.Progress).To(Equal("N/A")) -// Expect(vmd.Status.Capacity).To(Equal("")) -// Expect(vmd.Status.Target.PersistentVolumeClaim).To(Equal("")) -// -// dvName = vmd.Annotations[controller.AnnVMDDataVolume] -// dv, err := helper.FetchObject(ctx, types.NamespacedName{Name: dvName, Namespace: "test-ns"}, reconciler.Client, &cdiv1.DataVolume{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(dv).NotTo(BeNil()) -// } -// -// { -// dv, err := helper.FetchObject(ctx, types.NamespacedName{Name: dvName, Namespace: "test-ns"}, reconciler.Client, &cdiv1.DataVolume{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(dv).NotTo(BeNil()) -// dv.Status.Phase = cdiv1.Pending -// err = reconciler.Client.Status().Update(ctx, dv) -// Expect(err).NotTo(HaveOccurred()) -// -// err = reconcileExecutor.Execute(ctx, reconciler) -// Expect(err).NotTo(HaveOccurred()) -// -// vmd, err := helper.FetchObject(ctx, types.NamespacedName{Name: "test-vmd", Namespace: "test-ns"}, reconciler.Client, &virtv2.VirtualDisk{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(vmd).NotTo(BeNil()) -// Expect(vmd.Status.Phase).To(Equal(virtv2.DiskPending)) -// Expect(vmd.Status.Progress).To(Equal("N/A")) -// Expect(vmd.Status.Capacity).To(Equal("")) -// Expect(vmd.Status.Target.PersistentVolumeClaim).To(Equal("")) -// } -// -// { -// pv := &corev1.PersistentVolume{ -// ObjectMeta: metav1.ObjectMeta{ -// Namespace: "test-ns", -// Name: "test-pv", -// Labels: nil, -// Annotations: nil, -// }, -// Spec: corev1.PersistentVolumeSpec{}, -// Status: corev1.PersistentVolumeStatus{ -// Phase: corev1.VolumeBound, -// }, -// } -// err := reconciler.Client.Create(ctx, pv) -// Expect(err).NotTo(HaveOccurred()) -// -// pvc := &corev1.PersistentVolumeClaim{ -// ObjectMeta: metav1.ObjectMeta{ -// Namespace: "test-ns", -// Name: dvName, -// Labels: nil, -// Annotations: nil, -// }, -// Spec: corev1.PersistentVolumeClaimSpec{ -// StorageClass: util.GetPointer("local-path"), -// VolumeName: pv.Name, -// }, -// Status: corev1.PersistentVolumeClaimStatus{ -// Phase: corev1.ClaimBound, -// Capacity: corev1.ResourceList{ -// corev1.ResourceStorage: resource.MustParse("15Gi"), -// }, -// }, -// } -// err = reconciler.Client.Create(ctx, pvc) -// Expect(err).NotTo(HaveOccurred()) -// -// dv, err := helper.FetchObject(ctx, types.NamespacedName{Name: dvName, Namespace: "test-ns"}, reconciler.Client, &cdiv1.DataVolume{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(dv).NotTo(BeNil()) -// dv.Status.Phase = cdiv1.CloneInProgress -// dv.Status.Progress = "50%" -// err = reconciler.Client.Status().Update(ctx, dv) -// Expect(err).NotTo(HaveOccurred()) -// -// err = reconcileExecutor.Execute(ctx, reconciler) -// Expect(err).NotTo(HaveOccurred()) -// -// vmd, err := helper.FetchObject(ctx, types.NamespacedName{Name: "test-vmd", Namespace: "test-ns"}, reconciler.Client, &virtv2.VirtualDisk{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(vmd).NotTo(BeNil()) -// Expect(vmd.Status.Phase).To(Equal(virtv2.DiskProvisioning)) -// Expect(vmd.Status.Progress).To(Equal("50%")) -// Expect(vmd.Status.Capacity).To(Equal("15Gi")) -// Expect(vmd.Status.Target.PersistentVolumeClaim).To(Equal("")) -// } -// -// { -// dv, err := helper.FetchObject(ctx, types.NamespacedName{Name: dvName, Namespace: "test-ns"}, reconciler.Client, &cdiv1.DataVolume{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(dv).NotTo(BeNil()) -// dv.Status.Phase = cdiv1.Succeeded -// dv.Status.Progress = "100%" -// err = reconciler.Client.Status().Update(ctx, dv) -// Expect(err).NotTo(HaveOccurred()) -// -// err = reconcileExecutor.Execute(ctx, reconciler) -// Expect(err).NotTo(HaveOccurred()) -// -// vmd, err := helper.FetchObject(ctx, types.NamespacedName{Name: "test-vmd", Namespace: "test-ns"}, reconciler.Client, &virtv2.VirtualDisk{}) -// Expect(err).NotTo(HaveOccurred()) -// Expect(vmd).NotTo(BeNil()) -// Expect(vmd.Status.Phase).To(Equal(virtv2.DiskReady)) -// Expect(vmd.Status.Progress).To(Equal("100%")) -// Expect(vmd.Status.Capacity).To(Equal("15Gi")) -// Expect(vmd.Status.Target.PersistentVolumeClaim).To(Equal(dvName)) -// } -// }) -// }) diff --git a/images/virtualization-artifact/pkg/controller/vmd_uploader.go b/images/virtualization-artifact/pkg/controller/vmd_uploader.go deleted file mode 100644 index e60487d9b..000000000 --- a/images/virtualization-artifact/pkg/controller/vmd_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" - vmdutil "github.com/deckhouse/virtualization-controller/pkg/common/vmd" - 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 *VMDReconciler) startUploaderPod(ctx context.Context, state *VMDReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - vmd := state.VMD.Current() - - opts.Log.V(1).Info("Creating uploader POD for VD", "vd.Name", vmd.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, vmd, 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.NewCABundleForVMD(vmd.Spec.DataSource), r.dvcrSettings) -} - -// createUploaderSettings fills settings for the dvcr-uploader binary. -func (r *VMDReconciler) createUploaderSettings(state *VMDReconcilerState) *uploader.Settings { - vmd := state.VMD.Current() - settings := &uploader.Settings{ - Verbose: r.verbose, - } - - // Set DVCR destination settings. - dvcrDestImageName := r.dvcrSettings.RegistryImageForVMD(vmd.Name, vmd.Namespace) - uploader.ApplyDVCRDestinationSettings(settings, r.dvcrSettings, state.Supplements, dvcrDestImageName) - - // TODO Update proxy settings. - - return settings -} - -func (r *VMDReconciler) createUploaderPodSettings(state *VMDReconcilerState) *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: vmdutil.MakeOwnerReference(state.VMD.Current()), - ControllerName: vmdControllerName, - InstallerLabels: map[string]string{}, - ServiceName: uploaderSvc.Name, - } -} - -func (r *VMDReconciler) startUploaderService(ctx context.Context, state *VMDReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.V(1).Info("Creating uploader Service for VD", "vd.Name", state.VMD.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 *VMDReconciler) createUploaderServiceSettings(state *VMDReconcilerState) *uploader.ServiceSettings { - uploaderSvc := state.Supplements.UploaderService() - return &uploader.ServiceSettings{ - Name: uploaderSvc.Name, - Namespace: uploaderSvc.Namespace, - OwnerReference: vmdutil.MakeOwnerReference(state.VMD.Current()), - } -} - -func (r *VMDReconciler) startUploaderIngress(ctx context.Context, state *VMDReconcilerState, opts two_phase_reconciler.ReconcilerOptions) error { - opts.Log.V(1).Info("Creating uploader Ingress for VD", "vd.Name", state.VMD.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 *VMDReconciler) createUploaderIngressSettings(state *VMDReconcilerState) *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: vmdutil.MakeOwnerReference(state.VMD.Current()), - } -} diff --git a/images/virtualization-artifact/pkg/controller/vmd_webhook.go b/images/virtualization-artifact/pkg/controller/vmd_webhook.go deleted file mode 100644 index 780e85cec..000000000 --- a/images/virtualization-artifact/pkg/controller/vmd_webhook.go +++ /dev/null @@ -1,101 +0,0 @@ -/* -Copyright 2024 Flant JSC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "context" - "errors" - "fmt" - - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - "github.com/deckhouse/virtualization/api/core/v1alpha2" -) - -func NewVMDValidator(log logr.Logger) *VMDValidator { - return &VMDValidator{log: log} -} - -type VMDValidator struct { - log logr.Logger -} - -func (v *VMDValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { - vmd, ok := obj.(*v1alpha2.VirtualDisk) - if !ok { - return nil, fmt.Errorf("expected a new VirtualDisk but got a %T", obj) - } - - v.log.Info("Validating virtual disk", "spec.pvc.size", vmd.Spec.PersistentVolumeClaim.Size) - - if vmd.Spec.PersistentVolumeClaim.Size != nil && vmd.Spec.PersistentVolumeClaim.Size.IsZero() { - return nil, fmt.Errorf("virtual machine disk size must be greater than 0") - } - - if vmd.Spec.DataSource == nil && (vmd.Spec.PersistentVolumeClaim.Size == nil || vmd.Spec.PersistentVolumeClaim.Size.IsZero()) { - return nil, fmt.Errorf("if the data source is not specified, it's necessary to set spec.PersistentVolumeClaim.size to create blank virtual disk") - } - - return nil, nil -} - -func (v *VMDValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - newVMD, ok := newObj.(*v1alpha2.VirtualDisk) - if !ok { - return nil, fmt.Errorf("expected a new VirtualDisk but got a %T", newObj) - } - - oldVMD, ok := oldObj.(*v1alpha2.VirtualDisk) - if !ok { - return nil, fmt.Errorf("expected an old VirtualDisk but got a %T", oldObj) - } - - v.log.Info("Validating virtual disk", - "old.spec.pvc.size", oldVMD.Spec.PersistentVolumeClaim.Size, - "new.spec.pvc.size", newVMD.Spec.PersistentVolumeClaim.Size, - ) - - if newVMD.Spec.PersistentVolumeClaim.Size == oldVMD.Spec.PersistentVolumeClaim.Size { - return nil, nil - } - - if newVMD.Spec.PersistentVolumeClaim.Size == nil { - return nil, errors.New("spec.persistentVolumeClaim.size cannot be omitted once set") - } - - if newVMD.Spec.PersistentVolumeClaim.Size.IsZero() { - return nil, fmt.Errorf("virtual machine disk size must be greater than 0") - } - - if oldVMD.Spec.PersistentVolumeClaim.Size != nil && newVMD.Spec.PersistentVolumeClaim.Size.Cmp(*oldVMD.Spec.PersistentVolumeClaim.Size) == -1 { - return nil, fmt.Errorf( - "spec.persistentVolumeClaim.size value (%s) should be greater than or equal to the current value (%s)", - newVMD.Spec.PersistentVolumeClaim.Size.String(), - oldVMD.Spec.PersistentVolumeClaim.Size.String(), - ) - } - - return nil, nil -} - -func (v *VMDValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { - err := fmt.Errorf("misconfigured webhook rules: delete operation not implemented") - v.log.Error(err, "Ensure the correctness of ValidatingWebhookConfiguration") - return nil, nil -} diff --git a/images/virtualization-artifact/pkg/monitoring/metrics/virtualdisk/collector.go b/images/virtualization-artifact/pkg/monitoring/metrics/virtualdisk/collector.go index 6942b53d4..c23cbcf19 100644 --- a/images/virtualization-artifact/pkg/monitoring/metrics/virtualdisk/collector.go +++ b/images/virtualization-artifact/pkg/monitoring/metrics/virtualdisk/collector.go @@ -100,8 +100,9 @@ func (s *scraper) updateDiskStatusPhaseMetrics(disk virtv2.VirtualDisk) { {phase == virtv2.DiskProvisioning, string(virtv2.DiskProvisioning)}, {phase == virtv2.DiskReady, string(virtv2.DiskReady)}, {phase == virtv2.DiskFailed, string(virtv2.DiskFailed)}, - {phase == virtv2.DiskPVCLost, string(virtv2.DiskPVCLost)}, - {phase == virtv2.DiskUnknown, string(virtv2.DiskUnknown)}, + {phase == virtv2.DiskLost, string(virtv2.DiskLost)}, + {phase == virtv2.DiskResizing, string(virtv2.DiskResizing)}, + {phase == virtv2.DiskTerminating, string(virtv2.DiskTerminating)}, } desc := diskMetrics[MetricDiskStatusPhase] for _, p := range phases { diff --git a/images/virtualization-artifact/pkg/sdk/framework/helper/resource_builder.go b/images/virtualization-artifact/pkg/sdk/framework/helper/resource_builder.go index 7bae9ac7a..57eb38422 100644 --- a/images/virtualization-artifact/pkg/sdk/framework/helper/resource_builder.go +++ b/images/virtualization-artifact/pkg/sdk/framework/helper/resource_builder.go @@ -43,14 +43,7 @@ func NewResourceBuilder[T client.Object](resource T, opts ResourceBuilderOptions } func (b *ResourceBuilder[T]) SetOwnerRef(obj metav1.Object, gvk schema.GroupVersionKind) { - newOwnerRefs := util.SetArrayElem( - b.Resource.GetOwnerReferences(), - *metav1.NewControllerRef(obj, gvk), - func(v1, v2 metav1.OwnerReference) bool { - return v1.Name == v2.Name - }, false, - ) - b.Resource.SetOwnerReferences(newOwnerRefs) + SetOwnerRef(b.Resource, *metav1.NewControllerRef(obj, gvk)) } func (b *ResourceBuilder[T]) AddAnnotation(annotation, value string) { @@ -68,3 +61,20 @@ func (b *ResourceBuilder[T]) GetResource() T { func (b *ResourceBuilder[T]) IsResourceExists() bool { return b.ResourceExists } + +func SetOwnerRef(obj metav1.Object, ref metav1.OwnerReference) bool { + newOwnerRefs := util.SetArrayElem( + obj.GetOwnerReferences(), + ref, + func(v1, v2 metav1.OwnerReference) bool { + return v1.Name == v2.Name + }, false, + ) + + if len(newOwnerRefs) == len(obj.GetOwnerReferences()) { + return false + } + + obj.SetOwnerReferences(newOwnerRefs) + return true +} diff --git a/images/virtualization-artifact/pkg/sdk/framework/helper/util.go b/images/virtualization-artifact/pkg/sdk/framework/helper/util.go index 7d94ecaef..9b835c13d 100644 --- a/images/virtualization-artifact/pkg/sdk/framework/helper/util.go +++ b/images/virtualization-artifact/pkg/sdk/framework/helper/util.go @@ -77,6 +77,35 @@ func CleanupObject(ctx context.Context, client client.Client, obj client.Object) return nil } +func PurgeObject(ctx context.Context, client client.Client, obj client.Object) (bool, error) { + key := types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } + + if len(obj.GetFinalizers()) > 0 { + obj.SetFinalizers([]string{}) + + err := client.Update(ctx, obj) + if err != nil { + if k8serrors.IsNotFound(err) { + return false, nil + } + return false, fmt.Errorf("remove finalizers for %s during cleanup: %w", key, err) + } + } + + err := client.Delete(ctx, obj) + switch { + case err == nil: + return true, nil + case k8serrors.IsNotFound(err): + return false, nil + default: + return false, fmt.Errorf("delete object %s during cleanup: %w", key, err) + } +} + // CleanupByName searches object by its name, removes finalizers on object (if any) and then delete object. // obj must be a struct pointer so that obj can be updated with the response returned by the Server. func CleanupByName(ctx context.Context, client client.Client, key client.ObjectKey, obj client.Object) error { diff --git a/templates/virtualization-controller/rbac-for-us.yaml b/templates/virtualization-controller/rbac-for-us.yaml index 016c3b5bf..299bac819 100644 --- a/templates/virtualization-controller/rbac-for-us.yaml +++ b/templates/virtualization-controller/rbac-for-us.yaml @@ -28,6 +28,7 @@ rules: - delete - list - watch + - patch - apiGroups: - networking.k8s.io resources: @@ -50,6 +51,7 @@ rules: - update - delete - watch + - patch - apiGroups: - "" resources: @@ -60,6 +62,7 @@ rules: verbs: - patch - update + - patch - apiGroups: - coordination.k8s.io resources: @@ -72,6 +75,14 @@ rules: - update - patch - delete +- apiGroups: + - storage.k8s.io + resources: + - storageclasses + verbs: + - get + - list + - watch - apiGroups: - "" resources: @@ -97,6 +108,7 @@ rules: - delete - watch - list + - patch - apiGroups: - internal.virtualization.deckhouse.io resources: diff --git a/templates/virtualization-controller/validation-webhook.yaml b/templates/virtualization-controller/validation-webhook.yaml index dc71ac4ee..cbf071aa4 100644 --- a/templates/virtualization-controller/validation-webhook.yaml +++ b/templates/virtualization-controller/validation-webhook.yaml @@ -20,7 +20,7 @@ webhooks: {{ .Values.virtualization.internal.controller.cert.ca }} admissionReviewVersions: ["v1"] sideEffects: None - - name: "vmd.virtualization-controller.validate.d8-virtualization" + - name: "vd.virtualization-controller.validate.d8-virtualization" rules: - apiGroups: ["virtualization.deckhouse.io"] apiVersions: ["v1alpha2"] @@ -37,6 +37,23 @@ webhooks: {{ .Values.virtualization.internal.controller.cert.ca }} admissionReviewVersions: ["v1"] sideEffects: None + - name: "cvi.virtualization-controller.validate.d8-virtualization" + rules: + - apiGroups: [ "virtualization.deckhouse.io" ] + apiVersions: [ "v1alpha2" ] + operations: [ "UPDATE" ] + resources: [ "clustervirtualimages" ] + scope: "Cluster" + clientConfig: + service: + namespace: d8-{{ .Chart.Name }} + name: virtualization-controller + path: /validate-virtualization-deckhouse-io-v1alpha2-clustervirtualimage + port: 443 + caBundle: | + {{ .Values.virtualization.internal.controller.cert.ca }} + admissionReviewVersions: [ "v1" ] + sideEffects: None - name: "vmbda.virtualization-controller.validate.d8-virtualization" rules: - apiGroups: ["virtualization.deckhouse.io"]