From afb1bcb32f559baf21d04e748ecb6fedf49d2c68 Mon Sep 17 00:00:00 2001 From: yzamir Date: Wed, 12 Feb 2025 15:42:07 +0200 Subject: [PATCH] =?UTF-8?q?=1B[200~add=20new=20VolumeNameTemplate=20field?= =?UTF-8?q?=20to=20plan=20CR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: yzamir --- .../forklift.konveyor.io_migrations.yaml | 13 +++ .../crd/bases/forklift.konveyor.io_plans.yaml | 39 +++++++++ pkg/apis/forklift/v1beta1/plan.go | 18 ++++ pkg/apis/forklift/v1beta1/plan/vm.go | 12 +++ .../forklift/v1beta1/zz_generated.deepcopy.go | 15 ++++ .../plan/adapter/vsphere/builder.go | 42 +++++++++ pkg/controller/plan/validation.go | 87 ++++++++++++++++--- 7 files changed, 213 insertions(+), 13 deletions(-) diff --git a/operator/config/crd/bases/forklift.konveyor.io_migrations.yaml b/operator/config/crd/bases/forklift.konveyor.io_migrations.yaml index 814c94139..25ae5f8b9 100644 --- a/operator/config/crd/bases/forklift.konveyor.io_migrations.yaml +++ b/operator/config/crd/bases/forklift.konveyor.io_migrations.yaml @@ -539,6 +539,19 @@ spec: type: description: Type used to qualify the name. type: string + volumeNameTemplate: + description: |- + VolumeNameTemplate is a template for generating volume interface names in the target virtual machine. + It follows Go template syntax and has access to the following variables: + - .PVCName: name of the PVC mounted to the VM using this volume + - .VolumeIndex: sequential index of the volume interface (0-based) + Note: + - This template will override at the plan level template + - If not specified on VM level and on Plan leverl, default naming conventions will be used + Examples: + "disk-{{.VolumeIndex}}" + "pvc-{{.PVCName}}" + type: string warm: description: Warm migration status properties: diff --git a/operator/config/crd/bases/forklift.konveyor.io_plans.yaml b/operator/config/crd/bases/forklift.konveyor.io_plans.yaml index 66870fb7c..f7a1bf847 100644 --- a/operator/config/crd/bases/forklift.konveyor.io_plans.yaml +++ b/operator/config/crd/bases/forklift.konveyor.io_plans.yaml @@ -476,8 +476,34 @@ spec: type: description: Type used to qualify the name. type: string + volumeNameTemplate: + description: |- + VolumeNameTemplate is a template for generating volume interface names in the target virtual machine. + It follows Go template syntax and has access to the following variables: + - .PVCName: name of the PVC mounted to the VM using this volume + - .VolumeIndex: sequential index of the volume interface (0-based) + Note: + - This template will override at the plan level template + - If not specified on VM level and on Plan leverl, default naming conventions will be used + Examples: + "disk-{{.VolumeIndex}}" + "pvc-{{.PVCName}}" + type: string type: object type: array + volumeNameTemplate: + description: |- + VolumeNameTemplate is a template for generating volume interface names in the target virtual machine. + It follows Go template syntax and has access to the following variables: + - .PVCName: name of the PVC mounted to the VM using this volume + - .VolumeIndex: sequential index of the volume interface (0-based) + Note: + - This template can be overridden at the individual VM level + - If not specified on VM level and on Plan leverl, default naming conventions will be used + Examples: + "disk-{{.VolumeIndex}}" + "pvc-{{.PVCName}}" + type: string warm: description: Whether this is a warm migration. type: boolean @@ -1089,6 +1115,19 @@ spec: type: description: Type used to qualify the name. type: string + volumeNameTemplate: + description: |- + VolumeNameTemplate is a template for generating volume interface names in the target virtual machine. + It follows Go template syntax and has access to the following variables: + - .PVCName: name of the PVC mounted to the VM using this volume + - .VolumeIndex: sequential index of the volume interface (0-based) + Note: + - This template will override at the plan level template + - If not specified on VM level and on Plan leverl, default naming conventions will be used + Examples: + "disk-{{.VolumeIndex}}" + "pvc-{{.PVCName}}" + type: string warm: description: Warm migration status properties: diff --git a/pkg/apis/forklift/v1beta1/plan.go b/pkg/apis/forklift/v1beta1/plan.go index db5654096..ae2a6742e 100644 --- a/pkg/apis/forklift/v1beta1/plan.go +++ b/pkg/apis/forklift/v1beta1/plan.go @@ -70,6 +70,18 @@ type PlanSpec struct { // "{{if eq .DiskIndex .RootDiskIndex}}root{{else}}data{{end}}-{{.DiskIndex}}" // +optional PVCNameTemplate string `json:"pvcNameTemplate,omitempty"` + // VolumeNameTemplate is a template for generating volume interface names in the target virtual machine. + // It follows Go template syntax and has access to the following variables: + // - .PVCName: name of the PVC mounted to the VM using this volume + // - .VolumeIndex: sequential index of the volume interface (0-based) + // Note: + // - This template can be overridden at the individual VM level + // - If not specified on VM level and on Plan leverl, default naming conventions will be used + // Examples: + // "disk-{{.VolumeIndex}}" + // "pvc-{{.PVCName}}" + // +optional + VolumeNameTemplate string `json:"volumeNameTemplate,omitempty"` } // Find a planned VM. @@ -173,3 +185,9 @@ type PVCNameTemplateData struct { DiskIndex int `json:"diskIndex"` RootDiskIndex int `json:"rootDiskIndex"` } + +// VolumeNameTemplateData contains fields used in naming templates. +type VolumeNameTemplateData struct { + PVCName string `json:"pvcName,omitempty"` + VolumeIndex int `json:"volumeIndex,omitempty"` +} diff --git a/pkg/apis/forklift/v1beta1/plan/vm.go b/pkg/apis/forklift/v1beta1/plan/vm.go index 047a2fcae..df3b00684 100644 --- a/pkg/apis/forklift/v1beta1/plan/vm.go +++ b/pkg/apis/forklift/v1beta1/plan/vm.go @@ -52,6 +52,18 @@ type VM struct { // "{{if eq .DiskIndex .RootDiskIndex}}root{{else}}data{{end}}-{{.DiskIndex}}" // +optional PVCNameTemplate string `json:"pvcNameTemplate,omitempty"` + // VolumeNameTemplate is a template for generating volume interface names in the target virtual machine. + // It follows Go template syntax and has access to the following variables: + // - .PVCName: name of the PVC mounted to the VM using this volume + // - .VolumeIndex: sequential index of the volume interface (0-based) + // Note: + // - This template will override at the plan level template + // - If not specified on VM level and on Plan leverl, default naming conventions will be used + // Examples: + // "disk-{{.VolumeIndex}}" + // "pvc-{{.PVCName}}" + // +optional + VolumeNameTemplate string `json:"volumeNameTemplate,omitempty"` } // Find a Hook for the specified step. diff --git a/pkg/apis/forklift/v1beta1/zz_generated.deepcopy.go b/pkg/apis/forklift/v1beta1/zz_generated.deepcopy.go index 0aa888f66..040d376c5 100644 --- a/pkg/apis/forklift/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/forklift/v1beta1/zz_generated.deepcopy.go @@ -977,3 +977,18 @@ func (in *StoragePair) DeepCopy() *StoragePair { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeNameTemplateData) DeepCopyInto(out *VolumeNameTemplateData) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeNameTemplateData. +func (in *VolumeNameTemplateData) DeepCopy() *VolumeNameTemplateData { + if in == nil { + return nil + } + out := new(VolumeNameTemplateData) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/plan/adapter/vsphere/builder.go b/pkg/controller/plan/adapter/vsphere/builder.go index 83ee1b878..29ca6fa5e 100644 --- a/pkg/controller/plan/adapter/vsphere/builder.go +++ b/pkg/controller/plan/adapter/vsphere/builder.go @@ -815,6 +815,7 @@ func (r *Builder) removeSharedDisks(vm *model.VM) { func (r *Builder) mapDisks(vm *model.VM, vmRef ref.Ref, persistentVolumeClaims []*core.PersistentVolumeClaim, object *cnv.VirtualMachineSpec) { var kVolumes []cnv.Volume var kDisks []cnv.Disk + var templateErr error disks := vm.Disks sort.Slice(disks, func(i, j int) bool { @@ -842,6 +843,26 @@ func (r *Builder) mapDisks(vm *model.VM, vmRef ref.Ref, persistentVolumeClaims [ for i, disk := range disks { pvc := pvcMap[r.baseVolume(disk.File)] volumeName := fmt.Sprintf("vol-%v", i) + + // If the volume name template is set, use it to generate the volume name. + volumeNameTemplate := r.getVolumeNameTemplate(vm) + if volumeNameTemplate != "" { + // Create template data + templateData := api.VolumeNameTemplateData{ + PVCName: pvc.Name, + VolumeIndex: i, + } + + volumeName, templateErr = r.executeTemplate(volumeNameTemplate, &templateData) + if templateErr != nil { + // Failed to generate volume name using template + r.Log.Info("Failed to generate volume name using template, using default name", "template", volumeNameTemplate, "error", templateErr) + + // fallback to default name and reset error + volumeName = fmt.Sprintf("vol-%v", i) + } + } + volume := cnv.Volume{ Name: volumeName, VolumeSource: cnv.VolumeSource{ @@ -1184,3 +1205,24 @@ func (r *Builder) getPVCNameTemplate(vm *model.VM) string { return "" } + +// getVolumeNameTemplate returns the volume name template +func (r *Builder) getVolumeNameTemplate(vm *model.VM) string { + // Get plan VM + planVM := r.getPlanVM(vm) + if planVM == nil { + return "" + } + + // if vm.VolumeNameTemplate is set, use it + if planVM.VolumeNameTemplate != "" { + return planVM.VolumeNameTemplate + } + + // if planSpec.VolumeNameTemplate is set, use it + if r.Plan.Spec.VolumeNameTemplate != "" { + return r.Plan.Spec.VolumeNameTemplate + } + + return "" +} diff --git a/pkg/controller/plan/validation.go b/pkg/controller/plan/validation.go index 3a2b84d94..f1a22b9b4 100644 --- a/pkg/controller/plan/validation.go +++ b/pkg/controller/plan/validation.go @@ -171,6 +171,11 @@ func (r *Reconciler) validate(plan *api.Plan) error { return err } + // Validate volume name template + if err := r.validateVolumeNameTemplate(plan); err != nil { + return err + } + return nil } @@ -190,6 +195,22 @@ func (r *Reconciler) validatePVCNameTemplate(plan *api.Plan) error { return nil } +func (r *Reconciler) validateVolumeNameTemplate(plan *api.Plan) error { + if err := r.IsValidVolumeNameTemplate(plan.Spec.VolumeNameTemplate); err != nil { + invalidPVCNameTemplate := libcnd.Condition{ + Type: NotValid, + Status: True, + Category: Critical, + Message: "Volume name template is invalid.", + Items: []string{}, + } + + plan.Status.SetCondition(invalidPVCNameTemplate) + } + + return nil +} + func (r *Reconciler) validateOpenShiftVersion(plan *api.Plan) error { source := plan.Referenced.Provider.Source if source == nil { @@ -484,6 +505,13 @@ func (r *Reconciler) validateVM(plan *api.Plan) error { Message: "VM PVC name template is invalid.", Items: []string{}, } + volumeNameInvalid := libcnd.Condition{ + Type: NotValid, + Status: True, + Category: Critical, + Message: "VM volume name template is invalid.", + Items: []string{}, + } setOf := map[string]bool{} // @@ -625,6 +653,12 @@ func (r *Reconciler) validateVM(plan *api.Plan) error { pvcNameInvalid.Items = append(pvcNameInvalid.Items, ref.String()) } } + // is valid vm pvc name template + if plan.Spec.VMs[i].VolumeNameTemplate != "" { + if err := r.IsValidVolumeNameTemplate(plan.Spec.VMs[i].VolumeNameTemplate); err != nil { + volumeNameInvalid.Items = append(volumeNameInvalid.Items, ref.String()) + } + } } if len(notFound.Items) > 0 { plan.Status.SetCondition(notFound) @@ -665,6 +699,9 @@ func (r *Reconciler) validateVM(plan *api.Plan) error { if len(pvcNameInvalid.Items) > 0 { plan.Status.SetCondition(pvcNameInvalid) } + if len(volumeNameInvalid.Items) > 0 { + plan.Status.SetCondition(volumeNameInvalid) + } return nil } @@ -1176,6 +1213,28 @@ func (r *Reconciler) checkOCPVersion(clientset kubernetes.Interface) error { return nil } +func (r *Reconciler) IsValidTemplate(templateStr string, testData interface{}) (string, error) { + // Validate golang template syntax + tmpl, err := template.New("template").Parse(templateStr) + if err != nil { + return "", liberr.Wrap(err, "Invalid template syntax") + } + + var buf bytes.Buffer + err = tmpl.Execute(&buf, testData) + if err != nil { + return "", liberr.Wrap(err, "Template execution failed") + } + result := buf.String() + + // Empty output is not valid + if result == "" { + return "", liberr.New("Template output is empty") + } + + return result, nil +} + func (r *Reconciler) IsValidPVCNameTemplate(pvcNameTemplate string) error { if pvcNameTemplate == "" { return nil @@ -1203,24 +1262,26 @@ func (r *Reconciler) IsValidPVCNameTemplate(pvcNameTemplate string) error { return nil } -func (r *Reconciler) IsValidTemplate(templateStr string, testData interface{}) (string, error) { - // Validate golang template syntax - tmpl, err := template.New("template").Parse(templateStr) - if err != nil { - return "", liberr.Wrap(err, "Invalid template syntax") +func (r *Reconciler) IsValidVolumeNameTemplate(volumeNameTemplate string) error { + if volumeNameTemplate == "" { + return nil } - var buf bytes.Buffer - err = tmpl.Execute(&buf, testData) + testData := api.VolumeNameTemplateData{ + PVCName: "test-pvc", + VolumeIndex: 0, + } + + result, err := r.IsValidTemplate(volumeNameTemplate, testData) if err != nil { - return "", liberr.Wrap(err, "Template execution failed") + return err } - result := buf.String() - // Empty output is not valid - if result == "" { - return "", liberr.New("Template output is empty") + // Validate that template output is a valid k8s label + errs := k8svalidation.IsValidLabelValue(result) + if len(errs) > 0 { + return liberr.New("Template output is not a valid k8s label", "errors", errs) } - return result, nil + return nil }