From f105e6ad824e60108a9bb93bac69e4e2646702d8 Mon Sep 17 00:00:00 2001 From: Marcin Franczyk Date: Thu, 15 Oct 2020 13:47:13 +0200 Subject: [PATCH] allow to customize VM devices Signed-off-by: Marcin Franczyk --- .../templates/machine-class.yaml | 4 + charts/internal/machine-class/values.yaml | 5 + docs/usage-as-end-user.md | 23 +++ hack/api-reference/api.md | 81 +++++++++ pkg/admission/validator/shoot.go | 47 ++++-- pkg/apis/kubevirt/types_worker.go | 15 +- pkg/apis/kubevirt/v1alpha1/types_worker.go | 19 +++ .../v1alpha1/zz_generated.conversion.go | 38 +++++ .../v1alpha1/zz_generated.deepcopy.go | 33 ++++ pkg/apis/kubevirt/validation/shoot.go | 3 + pkg/apis/kubevirt/validation/shoot_test.go | 14 ++ pkg/apis/kubevirt/validation/worker.go | 42 ++++- pkg/apis/kubevirt/validation/worker_test.go | 157 +++++++++++++++++- pkg/apis/kubevirt/zz_generated.deepcopy.go | 33 ++++ pkg/controller/worker/machines.go | 6 +- pkg/controller/worker/machines_test.go | 81 +++++++++ pkg/kubevirt/types.go | 3 + 17 files changed, 581 insertions(+), 23 deletions(-) diff --git a/charts/internal/machine-class/templates/machine-class.yaml b/charts/internal/machine-class/templates/machine-class.yaml index a5490065..9b878f1b 100644 --- a/charts/internal/machine-class/templates/machine-class.yaml +++ b/charts/internal/machine-class/templates/machine-class.yaml @@ -24,6 +24,10 @@ providerSpec: resources: {{ toYaml $machineClass.resources | indent 4 }} {{- end }} +{{- if $machineClass.devices }} + devices: +{{ toYaml $machineClass.devices | indent 4 }} +{{- end }} {{- if $machineClass.rootVolume }} rootVolume: {{ toYaml $machineClass.rootVolume | indent 4 }} diff --git a/charts/internal/machine-class/values.yaml b/charts/internal/machine-class/values.yaml index 4163abb1..b6ba140c 100644 --- a/charts/internal/machine-class/values.yaml +++ b/charts/internal/machine-class/values.yaml @@ -10,6 +10,11 @@ machineClasses: cpu: "300m" memory: "4Gi" overcommitGuestOverhead: true + devices: + disks: + - name: "root-disk" + cache: "none" + networkInterfaceMultiQueue: true rootVolume: pvc: storageClassName: standard diff --git a/docs/usage-as-end-user.md b/docs/usage-as-end-user.md index 8a4f6b83..b9e0c922 100644 --- a/docs/usage-as-end-user.md +++ b/docs/usage-as-end-user.md @@ -137,6 +137,29 @@ An example `WorkerConfig` for the KubeVirt extension looks as follows: ```yaml apiVersion: kubevirt.provider.extensions.gardener.cloud/v1alpha1 kind: WorkerConfig +devices: + # disks allow to customize disks attached to KubeVirt VM + # check [link](https://kubevirt.io/user-guide/#/creation/disks-and-volumes?id=disks-and-volumes) for full specification and options + disks: + # name must match defined dataVolume name + # to modify root volume the name must be equal to 'root-disk' + - name: root-disk # modify root-disk + # disk type, check [link](https://kubevirt.io/user-guide/#/creation/disks-and-volumes?id=disks) for more types + disk: + # bus indicates the type of disk device to emulate. + bus: virtio + # set disk device cache + cache: writethrough + # dedicatedIOThread indicates this disk should have an exclusive IO Thread + dedicatedIOThread: true + - name: volume-1 # modify dataVolume named volume-1 + disk: {} + # whether to have random number generator from host + rng: {} + # whether or not to enable virtio multi-queue for block devices + blockMultiQueue: true + # if specified, virtual network interfaces configured with a virtio bus will also enable the vhost multiqueue feature + networkInterfaceMultiQueue: true cpu: # number of cores inside the VMI cores: 1 diff --git a/hack/api-reference/api.md b/hack/api-reference/api.md index d0bbddf4..959758a9 100644 --- a/hack/api-reference/api.md +++ b/hack/api-reference/api.md @@ -205,6 +205,20 @@ string +devices
+ + +Devices + + + + +(Optional) +

Devices allows to customize devices attached to KubeVirt VM

+ + + + cpu
kubevirt.io/client-go/api/v1.CPU @@ -375,6 +389,73 @@ map[string]bool +

Devices +

+

+(Appears on: +WorkerConfig) +

+

+

Devices allows to fine-tune devices attached to KubeVirt VM

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+disks
+ +[]kubevirt.io/client-go/api/v1.Disk + +
+(Optional) +

Disks allows to customize disks attached to KubeVirt VM

+
+rng
+ +kubevirt.io/client-go/api/v1.Rng + +
+(Optional) +

Whether to have random number generator from host

+
+blockMultiQueue
+ +bool + +
+(Optional) +

Whether or not to enable virtio multi-queue for block devices

+
+networkInterfaceMultiqueue
+ +bool + +
+(Optional) +

If specified, virtual network interfaces configured with a virtio bus will also enable the vhost multiqueue feature

+

InfrastructureStatus

diff --git a/pkg/admission/validator/shoot.go b/pkg/admission/validator/shoot.go index a8b1cf52..682f8506 100644 --- a/pkg/admission/validator/shoot.go +++ b/pkg/admission/validator/shoot.go @@ -93,13 +93,18 @@ var ( workerConfigPath = func(index int) *field.Path { return workersPath.Index(index).Child("providerConfig") } ) +type workerConfigContext struct { + workerConfig *apiskubevirt.WorkerConfig + dataVolumes []core.DataVolume +} + type validationContext struct { - shoot *core.Shoot - infrastructureConfig *apiskubevirt.InfrastructureConfig - controlPlaneConfig *apiskubevirt.ControlPlaneConfig - workerConfigs []*apiskubevirt.WorkerConfig - cloudProfile *gardencorev1beta1.CloudProfile - cloudProfileConfig *apiskubevirt.CloudProfileConfig + shoot *core.Shoot + infrastructureConfig *apiskubevirt.InfrastructureConfig + controlPlaneConfig *apiskubevirt.ControlPlaneConfig + workerConfigsContexts []*workerConfigContext + cloudProfile *gardencorev1beta1.CloudProfile + cloudProfileConfig *apiskubevirt.CloudProfileConfig } func (s *shoot) validateContext(valContext *validationContext) field.ErrorList { @@ -111,8 +116,8 @@ func (s *shoot) validateContext(valContext *validationContext) field.ErrorList { allErrors = append(allErrors, validation.ValidateInfrastructureConfig(valContext.infrastructureConfig, infrastructureConfigPath)...) allErrors = append(allErrors, validation.ValidateControlPlaneConfig(valContext.controlPlaneConfig, controlPlaneConfigPath)...) allErrors = append(allErrors, validation.ValidateWorkers(valContext.shoot.Spec.Provider.Workers, workersPath)...) - for i, workerConfig := range valContext.workerConfigs { - allErrors = append(allErrors, validation.ValidateWorkerConfig(workerConfig, workerConfigPath(i))...) + for i, workerConfigContext := range valContext.workerConfigsContexts { + allErrors = append(allErrors, validation.ValidateWorkerConfig(workerConfigContext.workerConfig, workerConfigContext.dataVolumes, workerConfigPath(i))...) } return allErrors @@ -158,8 +163,10 @@ func (s *shoot) validateUpdate(ctx context.Context, oldShoot, shoot *core.Shoot) allErrors = append(allErrors, validation.ValidateWorkersUpdate(oldValContext.shoot.Spec.Provider.Workers, currentValContext.shoot.Spec.Provider.Workers, workersPath)...) - for i, currentWorkerConfig := range currentValContext.workerConfigs { - for j, oldWorkerConfig := range oldValContext.workerConfigs { + for i, currentContext := range currentValContext.workerConfigsContexts { + currentWorkerConfig := currentContext.workerConfig + for j, oldContext := range oldValContext.workerConfigsContexts { + oldWorkerConfig := oldContext.workerConfig if shoot.Spec.Provider.Workers[i].Name == oldShoot.Spec.Provider.Workers[j].Name && !reflect.DeepEqual(oldWorkerConfig, currentWorkerConfig) { allErrors = append(allErrors, validation.ValidateWorkerConfigUpdate(currentWorkerConfig, oldWorkerConfig, workerConfigPath(i))...) } @@ -191,7 +198,7 @@ func (s *shoot) newValidationContext(ctx context.Context, shoot *core.Shoot) (*v } } - var workerConfigs []*apiskubevirt.WorkerConfig + var workerConfigsContexts []*workerConfigContext for _, worker := range shoot.Spec.Provider.Workers { workerConfig := &apiskubevirt.WorkerConfig{} if worker.ProviderConfig != nil { @@ -201,7 +208,11 @@ func (s *shoot) newValidationContext(ctx context.Context, shoot *core.Shoot) (*v return nil, errors.Wrapf(err, "could not decode providerConfig of worker %q", worker.Name) } } - workerConfigs = append(workerConfigs, workerConfig) + + workerConfigsContexts = append(workerConfigsContexts, &workerConfigContext{ + workerConfig: workerConfig, + dataVolumes: worker.DataVolumes, + }) } cloudProfile := &gardencorev1beta1.CloudProfile{} @@ -218,12 +229,12 @@ func (s *shoot) newValidationContext(ctx context.Context, shoot *core.Shoot) (*v } return &validationContext{ - shoot: shoot, - infrastructureConfig: infrastructureConfig, - controlPlaneConfig: controlPlaneConfig, - workerConfigs: workerConfigs, - cloudProfile: cloudProfile, - cloudProfileConfig: cloudProfileConfig, + shoot: shoot, + infrastructureConfig: infrastructureConfig, + controlPlaneConfig: controlPlaneConfig, + workerConfigsContexts: workerConfigsContexts, + cloudProfile: cloudProfile, + cloudProfileConfig: cloudProfileConfig, }, nil } diff --git a/pkg/apis/kubevirt/types_worker.go b/pkg/apis/kubevirt/types_worker.go index 1ded4c44..9f130728 100644 --- a/pkg/apis/kubevirt/types_worker.go +++ b/pkg/apis/kubevirt/types_worker.go @@ -26,8 +26,9 @@ import ( type WorkerConfig struct { metav1.TypeMeta + // Devices allows to customize devices attached to KubeVirt VM + Devices *Devices // CPU allows specifying the CPU topology of KubeVirt VM. - // +optional CPU *kubevirtv1.CPU // Memory allows specifying the VirtualMachineInstance memory features like huge pages and guest memory settings. // Each feature might require appropriate FeatureGate enabled. @@ -54,6 +55,18 @@ type WorkerConfig struct { OvercommitGuestOverhead bool } +// Devices allows to fine-tune devices attached to KubeVirt VM +type Devices struct { + // Disks allows to customize disks attached to KubeVirt VM + Disks []kubevirtv1.Disk + // Whether to have random number generator from host + Rng *kubevirtv1.Rng + // Whether or not to enable virtio multi-queue for block devices + BlockMultiQueue bool + // If specified, virtual network interfaces configured with a virtio bus will also enable the vhost multiqueue feature + NetworkInterfaceMultiQueue bool +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // WorkerStatus contains information about created worker resources. diff --git a/pkg/apis/kubevirt/v1alpha1/types_worker.go b/pkg/apis/kubevirt/v1alpha1/types_worker.go index 50214e58..4f332f52 100644 --- a/pkg/apis/kubevirt/v1alpha1/types_worker.go +++ b/pkg/apis/kubevirt/v1alpha1/types_worker.go @@ -27,6 +27,9 @@ import ( type WorkerConfig struct { metav1.TypeMeta `json:",inline"` + // Devices allows to customize devices attached to KubeVirt VM + // +optional + Devices *Devices `json:"devices,omitempty"` // CPU allows specifying the CPU topology of KubeVirt VM. // +optional CPU *kubevirtv1.CPU `json:"cpu,omitempty"` @@ -60,6 +63,22 @@ type WorkerConfig struct { OvercommitGuestOverhead bool `json:"overcommitGuestOverhead,omitempty"` } +// Devices allows to fine-tune devices attached to KubeVirt VM +type Devices struct { + // Disks allows to customize disks attached to KubeVirt VM + // +optional + Disks []kubevirtv1.Disk `json:"disks,omitempty"` + // Whether to have random number generator from host + // +optional + Rng *kubevirtv1.Rng `json:"rng,omitempty"` + // Whether or not to enable virtio multi-queue for block devices + // +optional + BlockMultiQueue bool `json:"blockMultiQueue,omitempty"` + // If specified, virtual network interfaces configured with a virtio bus will also enable the vhost multiqueue feature + // +optional + NetworkInterfaceMultiQueue bool `json:"networkInterfaceMultiqueue,omitempty"` +} + // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/kubevirt/v1alpha1/zz_generated.conversion.go b/pkg/apis/kubevirt/v1alpha1/zz_generated.conversion.go index 37977ba4..b4d27226 100644 --- a/pkg/apis/kubevirt/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/kubevirt/v1alpha1/zz_generated.conversion.go @@ -67,6 +67,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*Devices)(nil), (*kubevirt.Devices)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Devices_To_kubevirt_Devices(a.(*Devices), b.(*kubevirt.Devices), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*kubevirt.Devices)(nil), (*Devices)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_kubevirt_Devices_To_v1alpha1_Devices(a.(*kubevirt.Devices), b.(*Devices), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*InfrastructureConfig)(nil), (*kubevirt.InfrastructureConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_InfrastructureConfig_To_kubevirt_InfrastructureConfig(a.(*InfrastructureConfig), b.(*kubevirt.InfrastructureConfig), scope) }); err != nil { @@ -262,6 +272,32 @@ func Convert_kubevirt_ControlPlaneConfig_To_v1alpha1_ControlPlaneConfig(in *kube return autoConvert_kubevirt_ControlPlaneConfig_To_v1alpha1_ControlPlaneConfig(in, out, s) } +func autoConvert_v1alpha1_Devices_To_kubevirt_Devices(in *Devices, out *kubevirt.Devices, s conversion.Scope) error { + out.Disks = *(*[]v1.Disk)(unsafe.Pointer(&in.Disks)) + out.Rng = (*v1.Rng)(unsafe.Pointer(in.Rng)) + out.BlockMultiQueue = in.BlockMultiQueue + out.NetworkInterfaceMultiQueue = in.NetworkInterfaceMultiQueue + return nil +} + +// Convert_v1alpha1_Devices_To_kubevirt_Devices is an autogenerated conversion function. +func Convert_v1alpha1_Devices_To_kubevirt_Devices(in *Devices, out *kubevirt.Devices, s conversion.Scope) error { + return autoConvert_v1alpha1_Devices_To_kubevirt_Devices(in, out, s) +} + +func autoConvert_kubevirt_Devices_To_v1alpha1_Devices(in *kubevirt.Devices, out *Devices, s conversion.Scope) error { + out.Disks = *(*[]v1.Disk)(unsafe.Pointer(&in.Disks)) + out.Rng = (*v1.Rng)(unsafe.Pointer(in.Rng)) + out.BlockMultiQueue = in.BlockMultiQueue + out.NetworkInterfaceMultiQueue = in.NetworkInterfaceMultiQueue + return nil +} + +// Convert_kubevirt_Devices_To_v1alpha1_Devices is an autogenerated conversion function. +func Convert_kubevirt_Devices_To_v1alpha1_Devices(in *kubevirt.Devices, out *Devices, s conversion.Scope) error { + return autoConvert_kubevirt_Devices_To_v1alpha1_Devices(in, out, s) +} + func autoConvert_v1alpha1_InfrastructureConfig_To_kubevirt_InfrastructureConfig(in *InfrastructureConfig, out *kubevirt.InfrastructureConfig, s conversion.Scope) error { if err := Convert_v1alpha1_NetworksConfig_To_kubevirt_NetworksConfig(&in.Networks, &out.Networks, s); err != nil { return err @@ -511,6 +547,7 @@ func Convert_kubevirt_TenantNetwork_To_v1alpha1_TenantNetwork(in *kubevirt.Tenan } func autoConvert_v1alpha1_WorkerConfig_To_kubevirt_WorkerConfig(in *WorkerConfig, out *kubevirt.WorkerConfig, s conversion.Scope) error { + out.Devices = (*kubevirt.Devices)(unsafe.Pointer(in.Devices)) out.CPU = (*v1.CPU)(unsafe.Pointer(in.CPU)) out.Memory = (*v1.Memory)(unsafe.Pointer(in.Memory)) out.DNSPolicy = corev1.DNSPolicy(in.DNSPolicy) @@ -526,6 +563,7 @@ func Convert_v1alpha1_WorkerConfig_To_kubevirt_WorkerConfig(in *WorkerConfig, ou } func autoConvert_kubevirt_WorkerConfig_To_v1alpha1_WorkerConfig(in *kubevirt.WorkerConfig, out *WorkerConfig, s conversion.Scope) error { + out.Devices = (*Devices)(unsafe.Pointer(in.Devices)) out.CPU = (*v1.CPU)(unsafe.Pointer(in.CPU)) out.Memory = (*v1.Memory)(unsafe.Pointer(in.Memory)) out.DNSPolicy = corev1.DNSPolicy(in.DNSPolicy) diff --git a/pkg/apis/kubevirt/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/kubevirt/v1alpha1/zz_generated.deepcopy.go index c73dc8db..271fa756 100644 --- a/pkg/apis/kubevirt/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/kubevirt/v1alpha1/zz_generated.deepcopy.go @@ -118,6 +118,34 @@ func (in *ControlPlaneConfig) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Devices) DeepCopyInto(out *Devices) { + *out = *in + if in.Disks != nil { + in, out := &in.Disks, &out.Disks + *out = make([]v1.Disk, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Rng != nil { + in, out := &in.Rng, &out.Rng + *out = new(v1.Rng) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Devices. +func (in *Devices) DeepCopy() *Devices { + if in == nil { + return nil + } + out := new(Devices) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InfrastructureConfig) DeepCopyInto(out *InfrastructureConfig) { *out = *in @@ -344,6 +372,11 @@ func (in *TenantNetwork) DeepCopy() *TenantNetwork { func (in *WorkerConfig) DeepCopyInto(out *WorkerConfig) { *out = *in out.TypeMeta = in.TypeMeta + if in.Devices != nil { + in, out := &in.Devices, &out.Devices + *out = new(Devices) + (*in).DeepCopyInto(*out) + } if in.CPU != nil { in, out := &in.CPU, &out.CPU *out = new(v1.CPU) diff --git a/pkg/apis/kubevirt/validation/shoot.go b/pkg/apis/kubevirt/validation/shoot.go index 1e120bcc..f01fb344 100644 --- a/pkg/apis/kubevirt/validation/shoot.go +++ b/pkg/apis/kubevirt/validation/shoot.go @@ -82,6 +82,9 @@ func validateVolume(vol *core.Volume, fldPath *field.Path) field.ErrorList { func validateDataVolume(vol *core.DataVolume, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} + if vol.Name == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must not be empty")) + } if vol.Type == nil { allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty")) } diff --git a/pkg/apis/kubevirt/validation/shoot_test.go b/pkg/apis/kubevirt/validation/shoot_test.go index 42ed6c09..8d2e5680 100644 --- a/pkg/apis/kubevirt/validation/shoot_test.go +++ b/pkg/apis/kubevirt/validation/shoot_test.go @@ -69,6 +69,7 @@ var _ = Describe("Shoot validation", func() { }, DataVolumes: []core.DataVolume{ { + Name: "volume-1", Type: pointer.StringPtr("DataVolume"), VolumeSize: "20G", }, @@ -124,6 +125,19 @@ var _ = Describe("Shoot validation", func() { )) }) + It("should forbid because worker data volume does not have a name", func() { + workers[0].DataVolumes[0].Name = "" + + errorList := ValidateWorkers(workers, nilPath) + + Expect(errorList).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("[0].dataVolumes[0].name"), + })), + )) + }) + It("should forbid because worker data volume does not have a type", func() { workers[0].DataVolumes[0].Type = nil diff --git a/pkg/apis/kubevirt/validation/worker.go b/pkg/apis/kubevirt/validation/worker.go index 3defb3d3..32812ab1 100644 --- a/pkg/apis/kubevirt/validation/worker.go +++ b/pkg/apis/kubevirt/validation/worker.go @@ -18,13 +18,16 @@ import ( "fmt" apiskubevirt "github.com/gardener/gardener-extension-provider-kubevirt/pkg/apis/kubevirt" + "github.com/gardener/gardener-extension-provider-kubevirt/pkg/kubevirt" + gardenercore "github.com/gardener/gardener/pkg/apis/core" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" ) // ValidateWorkerConfig validates a WorkerConfig object. -func ValidateWorkerConfig(config *apiskubevirt.WorkerConfig, fldPath *field.Path) field.ErrorList { +func ValidateWorkerConfig(config *apiskubevirt.WorkerConfig, dataVolumes []gardenercore.DataVolume, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if config.DNSPolicy != "" { @@ -51,6 +54,34 @@ func ValidateWorkerConfig(config *apiskubevirt.WorkerConfig, fldPath *field.Path } } + if config.Devices != nil { + disksPath := fldPath.Child("devices").Child("disks") + disks := sets.NewString() + + // +1 because of root-disk which is required and unique + volumesLen := len(dataVolumes) + 1 + + if disksLen := len(config.Devices.Disks); disksLen > volumesLen { + allErrs = append(allErrs, field.Invalid(disksPath, disksLen, "the number of disks is larger than the number of volumes")) + } + + for i, disk := range config.Devices.Disks { + if disk.BootOrder != nil { + allErrs = append(allErrs, field.Forbidden(disksPath.Index(i).Child("bootOrder"), "cannot be set")) + } + + if disk.Name == "" { + allErrs = append(allErrs, field.Required(disksPath.Index(i).Child("name"), "cannot be empty")) + } else if disks.Has(disk.Name) { + allErrs = append(allErrs, field.Invalid(disksPath.Index(i).Child("name"), disk.Name, "already exists")) + continue + } else if !hasDiskVolumeMatch(disk.Name, dataVolumes) && disk.Name != kubevirt.RootDiskName { + allErrs = append(allErrs, field.Invalid(disksPath.Index(i).Child("name"), disk.Name, "no matching volume")) + } + disks.Insert(disk.Name) + } + } + return allErrs } @@ -59,3 +90,12 @@ func ValidateWorkerConfigUpdate(oldConfig, newConfig *apiskubevirt.WorkerConfig, allErrs := field.ErrorList{} return allErrs } + +func hasDiskVolumeMatch(diskName string, volumes []gardenercore.DataVolume) bool { + for _, volume := range volumes { + if volume.Name == diskName { + return true + } + } + return false +} diff --git a/pkg/apis/kubevirt/validation/worker_test.go b/pkg/apis/kubevirt/validation/worker_test.go index 17c70f86..2d3952c7 100644 --- a/pkg/apis/kubevirt/validation/worker_test.go +++ b/pkg/apis/kubevirt/validation/worker_test.go @@ -17,7 +17,9 @@ package validation_test import ( apiskubevirt "github.com/gardener/gardener-extension-provider-kubevirt/pkg/apis/kubevirt" . "github.com/gardener/gardener-extension-provider-kubevirt/pkg/apis/kubevirt/validation" + "github.com/gardener/gardener-extension-provider-kubevirt/pkg/kubevirt" + gardenercore "github.com/gardener/gardener/pkg/apis/core" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" @@ -25,6 +27,7 @@ import ( gomegatypes "github.com/onsi/gomega/types" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/validation/field" + kubevirtv1 "kubevirt.io/client-go/api/v1" ) var _ = Describe("WorkerConfig validation", func() { @@ -44,7 +47,7 @@ var _ = Describe("WorkerConfig validation", func() { func(dnsPolicy corev1.DNSPolicy, dnsConfig *corev1.PodDNSConfig, matcher gomegatypes.GomegaMatcher) { config.DNSPolicy = dnsPolicy config.DNSConfig = dnsConfig - err := ValidateWorkerConfig(config, nilPath) + err := ValidateWorkerConfig(config, nil, nilPath) Expect(err).To(matcher) }, @@ -77,5 +80,157 @@ var _ = Describe("WorkerConfig validation", func() { ), ) + bootOrder0 := uint(0) + DescribeTable("#ValidateDisksAndVolumes", + func(disks []kubevirtv1.Disk, dataVolumes []gardenercore.DataVolume, matcher gomegatypes.GomegaMatcher) { + config.Devices = &apiskubevirt.Devices{ + Disks: disks, + } + err := ValidateWorkerConfig(config, dataVolumes, nilPath) + Expect(err).To(matcher) + }, + Entry("should not return error with appropriate disks and volumes match", + []kubevirtv1.Disk{ + { + Name: "disk-1", + }, + { + Name: "disk-2", + }, + }, + []gardenercore.DataVolume{ + { + Name: "disk-1", + }, + { + Name: "disk-2", + }, + }, + Equal(field.ErrorList{}), + ), + Entry("should return error with empty disk name", + []kubevirtv1.Disk{ + { + Name: "", + }, + }, + []gardenercore.DataVolume{ + { + Name: "disk-1", + }, + }, + ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("devices.disks[0].name"), + }))), + ), + Entry("should return error with disks and volumes that do not match", + []kubevirtv1.Disk{ + { + Name: "disk-1a", + }, + }, + []gardenercore.DataVolume{ + { + Name: "disk-1b", + }, + }, + ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("devices.disks[0].name"), + }))), + ), + Entry("should return error with number of disks bigger than volumes", + []kubevirtv1.Disk{ + { + Name: kubevirt.RootDiskName, + }, + { + Name: "disk-1", + }, + { + Name: "disk-2", + }, + }, + []gardenercore.DataVolume{ + { + Name: "disk-2", + }, + }, + ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("devices.disks"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("devices.disks[1].name"), + })), + ), + ), + Entry("should return error with duplicated disks names", + []kubevirtv1.Disk{ + { + Name: kubevirt.RootDiskName, + }, + { + Name: kubevirt.RootDiskName, + }, + { + Name: "disk-1", + }, + { + Name: "disk-1", + }, + }, + []gardenercore.DataVolume{ + { + Name: "disk-1", + }, + { + Name: "disk-2", + }, + { + Name: "disk-3", + }, + }, + ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("devices.disks[1].name"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("devices.disks[3].name"), + })), + ), + ), + Entry("should return error when boot order is set for disk", + []kubevirtv1.Disk{ + { + Name: "disk-1", + }, + { + Name: "disk-2", + BootOrder: &bootOrder0, + }, + }, + []gardenercore.DataVolume{ + { + Name: "disk-1", + }, + { + Name: "disk-2", + }, + }, + ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeForbidden), + "Field": Equal("devices.disks[1].bootOrder"), + })), + ), + ), + ) + }) }) diff --git a/pkg/apis/kubevirt/zz_generated.deepcopy.go b/pkg/apis/kubevirt/zz_generated.deepcopy.go index ac6990f9..2b36fb13 100644 --- a/pkg/apis/kubevirt/zz_generated.deepcopy.go +++ b/pkg/apis/kubevirt/zz_generated.deepcopy.go @@ -118,6 +118,34 @@ func (in *ControlPlaneConfig) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Devices) DeepCopyInto(out *Devices) { + *out = *in + if in.Disks != nil { + in, out := &in.Disks, &out.Disks + *out = make([]v1.Disk, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Rng != nil { + in, out := &in.Rng, &out.Rng + *out = new(v1.Rng) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Devices. +func (in *Devices) DeepCopy() *Devices { + if in == nil { + return nil + } + out := new(Devices) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InfrastructureConfig) DeepCopyInto(out *InfrastructureConfig) { *out = *in @@ -344,6 +372,11 @@ func (in *TenantNetwork) DeepCopy() *TenantNetwork { func (in *WorkerConfig) DeepCopyInto(out *WorkerConfig) { *out = *in out.TypeMeta = in.TypeMeta + if in.Devices != nil { + in, out := &in.Devices, &out.Devices + *out = new(Devices) + (*in).DeepCopyInto(*out) + } if in.CPU != nil { in, out := &in.CPU, &out.CPU *out = new(v1.CPU) diff --git a/pkg/controller/worker/machines.go b/pkg/controller/worker/machines.go index 3a8d69fc..0c56c369 100644 --- a/pkg/controller/worker/machines.go +++ b/pkg/controller/worker/machines.go @@ -188,12 +188,13 @@ func (w *workerDelegate) generateMachineConfig(ctx context.Context) error { // Build additional volumes var additionalVolumes []map[string]interface{} for _, volume := range pool.DataVolumes { - className, size, err := w.getStorageClassNameAndSize(*volume.Type, volume.Size) + storageClassName, size, err := w.getStorageClassNameAndSize(*volume.Type, volume.Size) if err != nil { return err } additionalVolumes = append(additionalVolumes, map[string]interface{}{ - "dataVolume": buildDataVolumeSpecWithBlankSource(className, size), + "name": volume.Name, + "dataVolume": buildDataVolumeSpecWithBlankSource(storageClassName, size), }) } @@ -217,6 +218,7 @@ func (w *workerDelegate) generateMachineConfig(ctx context.Context) error { "region": w.worker.Spec.Region, "zone": zone, "resources": resourceRequirements, + "devices": workerConfig.Devices, "rootVolume": rootVolume, "additionalVolumes": additionalVolumes, "sshKeys": []string{string(w.worker.Spec.SSHPublicKey)}, diff --git a/pkg/controller/worker/machines_test.go b/pkg/controller/worker/machines_test.go index 474aa090..5ac35295 100644 --- a/pkg/controller/worker/machines_test.go +++ b/pkg/controller/worker/machines_test.go @@ -124,6 +124,8 @@ var _ = Describe("Machines", func() { networkName := "default/net-conf" networkSHA := "abc" dnsNameserver := "8.8.8.8" + rootDiskName := "root-disk" + additionalDataVolumeName := "dv1" images := []kubevirtv1alpha1.MachineImages{ { @@ -182,6 +184,19 @@ var _ = Describe("Machines", func() { APIVersion: "kubevirt.provider.extensions.gardener.cloud/v1alpha1", Kind: "WorkerConfig", }, + Devices: &kubevirtv1alpha1.Devices{ + Disks: []kubevirtv1.Disk{ + { + Name: rootDiskName, + DiskDevice: kubevirtv1.DiskDevice{ + Disk: &kubevirtv1.DiskTarget{ + Bus: "virtio", + }, + }, + Cache: kubevirtv1.CacheNone, + }, + }, + }, CPU: &kubevirtv1.CPU{ Cores: uint32(1), Sockets: uint32(2), @@ -219,12 +234,35 @@ var _ = Describe("Machines", func() { }, DataVolumes: []extensionsv1alpha1.DataVolume{ { + Name: additionalDataVolumeName, Type: pointer.StringPtr("standard"), Size: "10Gi", }, }, UserData: userData, Zones: []string{"local-3"}, + ProviderConfig: &runtime.RawExtension{ + Raw: encode(&kubevirtv1alpha1.WorkerConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kubevirt.provider.extensions.gardener.cloud/v1alpha1", + Kind: "WorkerConfig", + }, + Devices: &kubevirtv1alpha1.Devices{ + Disks: []kubevirtv1.Disk{ + { + Name: additionalDataVolumeName, + DiskDevice: kubevirtv1.DiskDevice{ + Disk: &kubevirtv1.DiskTarget{ + Bus: "virtio", + }, + }, + Cache: kubevirtv1.CacheNone, + }, + }, + NetworkInterfaceMultiQueue: true, + }, + }), + }, }, }, SSHPublicKey: sshPublicKey, @@ -288,6 +326,19 @@ var _ = Describe("Machines", func() { }, OvercommitGuestOverhead: true, }, + &apiskubevirt.Devices{ + Disks: []kubevirtv1.Disk{ + { + Name: rootDiskName, + DiskDevice: kubevirtv1.DiskDevice{ + Disk: &kubevirtv1.DiskTarget{ + Bus: "virtio", + }, + }, + Cache: kubevirtv1.CacheNone, + }, + }, + }, &cdicorev1alpha1.DataVolumeSpec{ PVC: &corev1.PersistentVolumeClaimSpec{ AccessModes: []corev1.PersistentVolumeAccessMode{ @@ -344,6 +395,19 @@ var _ = Describe("Machines", func() { }, OvercommitGuestOverhead: true, }, + &apiskubevirt.Devices{ + Disks: []kubevirtv1.Disk{ + { + Name: rootDiskName, + DiskDevice: kubevirtv1.DiskDevice{ + Disk: &kubevirtv1.DiskTarget{ + Bus: "virtio", + }, + }, + Cache: kubevirtv1.CacheNone, + }, + }, + }, &cdicorev1alpha1.DataVolumeSpec{ PVC: &corev1.PersistentVolumeClaimSpec{ AccessModes: []corev1.PersistentVolumeAccessMode{ @@ -395,6 +459,20 @@ var _ = Describe("Machines", func() { corev1.ResourceMemory: resource.MustParse("8192Mi"), }, }, + &apiskubevirt.Devices{ + Disks: []kubevirtv1.Disk{ + { + Name: additionalDataVolumeName, + DiskDevice: kubevirtv1.DiskDevice{ + Disk: &kubevirtv1.DiskTarget{ + Bus: "virtio", + }, + }, + Cache: kubevirtv1.CacheNone, + }, + }, + NetworkInterfaceMultiQueue: true, + }, &cdicorev1alpha1.DataVolumeSpec{ PVC: &corev1.PersistentVolumeClaimSpec{ AccessModes: []corev1.PersistentVolumeAccessMode{ @@ -416,6 +494,7 @@ var _ = Describe("Machines", func() { }, []map[string]interface{}{ { + "name": additionalDataVolumeName, "dataVolume": &cdicorev1alpha1.DataVolumeSpec{ PVC: &corev1.PersistentVolumeClaimSpec{ AccessModes: []corev1.PersistentVolumeAccessMode{ @@ -651,6 +730,7 @@ func createMachineClass( classTemplate map[string]interface{}, name, zone string, resources *kubevirtv1.ResourceRequirements, + devices *apiskubevirt.Devices, rootVolume *cdicorev1alpha1.DataVolumeSpec, additionalVolumes []map[string]interface{}, cpu *kubevirtv1.CPU, @@ -667,6 +747,7 @@ func createMachineClass( out["name"] = name out["zone"] = zone out["resources"] = resources + out["devices"] = devices out["rootVolume"] = rootVolume out["additionalVolumes"] = additionalVolumes out["cpu"] = cpu diff --git a/pkg/kubevirt/types.go b/pkg/kubevirt/types.go index 9b667f6e..070da6af 100644 --- a/pkg/kubevirt/types.go +++ b/pkg/kubevirt/types.go @@ -48,6 +48,9 @@ const ( MachineControllerManagerMonitoringConfigName = "machine-controller-manager-monitoring-config" // MachineControllerManagerVpaName is the name of the VerticalPodAutoscaler of the machine-controller-manager deployment. MachineControllerManagerVpaName = "machine-controller-manager-vpa" + + // Root disk name + RootDiskName = "root-disk" ) var (