Skip to content

Commit dce982f

Browse files
authored
validate and merge storage controller configurations (#1315)
Add validation and merging logic for storage controller configurations from VM Class, VM Image (OVF), and user-specified hardware spec. The webhook validates conflicts during VM creation, while the provider merges controllers during VM provisioning. Controller conflicts are detected when controllers at the same bus number have incompatible properties (e.g., different SCSI SharingMode or controller subtypes). This prevents runtime errors by catching incompatible controller configurations early and ensures proper controller merging during VM creation.
1 parent 48b56c2 commit dce982f

13 files changed

+3229
-140
lines changed

pkg/providers/vsphere/vmprovider_vm.go

Lines changed: 51 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"github.com/go-logr/logr"
2020
"github.com/vmware/govmomi/fault"
2121
"github.com/vmware/govmomi/object"
22-
"github.com/vmware/govmomi/ovf"
2322
"github.com/vmware/govmomi/pbm"
2423
pbmtypes "github.com/vmware/govmomi/pbm/types"
2524
"github.com/vmware/govmomi/property"
@@ -34,7 +33,6 @@ import (
3433
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
3534
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3635
"sigs.k8s.io/controller-runtime/pkg/event"
37-
"sigs.k8s.io/yaml"
3836

3937
imgregv1a1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha1"
4038
imgregv1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha2"
@@ -2434,55 +2432,22 @@ func (vs *vSphereVMProvider) vmCreateGenConfigSpecImage(
24342432
}
24352433
}
24362434

2437-
var (
2438-
vmiCache vmopv1.VirtualMachineImageCache
2439-
vmiCacheKey = ctrlclient.ObjectKey{
2440-
Namespace: pkgcfg.FromContext(vmCtx).PodNamespace,
2441-
Name: pkgutil.VMIName(itemID),
2442-
}
2443-
)
2444-
2445-
// Get the VirtualMachineImageCache resource.
2446-
if err := vs.k8sClient.Get(
2435+
// Get and validate the VirtualMachineImageCache
2436+
vmiCache, err := GetVirtualMachineImageCache(
24472437
vmCtx,
2448-
vmiCacheKey,
2449-
&vmiCache); err != nil {
2450-
2451-
return fmt.Errorf(
2452-
"failed to get vmi cache object: %w, %w",
2453-
err,
2454-
pkgerr.VMICacheNotReadyError{Name: vmiCacheKey.Name})
2455-
}
2456-
2457-
// Check if the hardware is ready.
2458-
hardwareReadyCondition := pkgcnd.Get(
2459-
vmiCache,
2460-
vmopv1.VirtualMachineImageCacheConditionHardwareReady)
2461-
2462-
switch {
2463-
case hardwareReadyCondition != nil &&
2464-
hardwareReadyCondition.Status == metav1.ConditionFalse:
2465-
2466-
return fmt.Errorf(
2467-
"failed to get hardware: %s: %w",
2468-
hardwareReadyCondition.Message,
2469-
pkgerr.VMICacheNotReadyError{Name: vmiCache.Name})
2470-
2471-
case hardwareReadyCondition == nil,
2472-
hardwareReadyCondition.Status != metav1.ConditionTrue,
2473-
imageType == imageTypeOVF && vmiCache.Status.OVF == nil,
2474-
imageType == imageTypeOVF && vmiCache.Status.OVF.ProviderVersion != itemVersion:
2475-
2476-
return pkgerr.VMICacheNotReadyError{
2477-
Message: "hardware not ready",
2478-
Name: vmiCacheKey.Name,
2479-
}
2438+
vs.k8sClient,
2439+
itemID,
2440+
itemVersion,
2441+
imageType,
2442+
pkgcfg.FromContext(vmCtx).PodNamespace)
2443+
if err != nil {
2444+
return err
24802445
}
24812446

24822447
var imgConfigSpec vimtypes.VirtualMachineConfigSpec
24832448

24842449
if imageType == imageTypeOVF {
2485-
cs, err := vs.getConfigSpecFromOVF(vmCtx, vmiCache)
2450+
cs, err := GetConfigSpecFromOVFCache(vmCtx, vs.k8sClient, vmiCache)
24862451
if err != nil {
24872452
return err
24882453
}
@@ -2538,11 +2503,47 @@ func (vs *vSphereVMProvider) vmCreateGenConfigSpecImage(
25382503
}
25392504
}
25402505

2541-
// Inherit the image's disks and their controllers.
2542-
pkgutil.CopyStorageControllersAndDisks(
2543-
&createArgs.ConfigSpec,
2544-
imgConfigSpec,
2545-
createArgs.StorageProfileID)
2506+
if !pkgcfg.FromContext(vmCtx).Features.VMSharedDisks {
2507+
// Inherit the image's disks and their controllers.
2508+
pkgutil.CopyStorageControllersAndDisks(
2509+
&createArgs.ConfigSpec,
2510+
imgConfigSpec,
2511+
createArgs.StorageProfileID)
2512+
} else {
2513+
dstKeyToSrcKey, controllersToRemove, err :=
2514+
pkgutil.ValidateStorageControllerCompatibility(
2515+
createArgs.ConfigSpec,
2516+
imgConfigSpec)
2517+
2518+
if err != nil {
2519+
return fmt.Errorf(
2520+
"%s: %w", pkgutil.ControllerConflictVMClassImage, err)
2521+
}
2522+
2523+
pkgutil.MergeStorageControllersAndDisks(
2524+
&createArgs.ConfigSpec,
2525+
imgConfigSpec,
2526+
dstKeyToSrcKey,
2527+
controllersToRemove,
2528+
createArgs.StorageProfileID)
2529+
2530+
specControllersConfigSpec :=
2531+
pkgutil.CreateUserStorageControllersConfigSpec(
2532+
vmCtx,
2533+
vmCtx.VM,
2534+
createArgs.ConfigSpec)
2535+
2536+
// DeviceChanges for spec controllers will be reconciled by the virtual
2537+
// controller reconciler after creation; hence, the controllers are only
2538+
// validated here.
2539+
if _, _, err := pkgutil.ValidateStorageControllerCompatibility(
2540+
specControllersConfigSpec,
2541+
createArgs.ConfigSpec); err != nil {
2542+
2543+
return fmt.Errorf(
2544+
"%s: %w", pkgutil.ControllerConflictVMClassImageUser, err)
2545+
}
2546+
}
25462547

25472548
if pkgcfg.FromContext(vmCtx).Features.AllDisksArePVCs {
25482549
if err := vs.vmCreateGenConfigSpecImagePVCDataSourceRefs(
@@ -2687,42 +2688,6 @@ func (vs *vSphereVMProvider) updateDiskDeviceFromPVC(
26872688
return nil
26882689
}
26892690

2690-
func (vs *vSphereVMProvider) getConfigSpecFromOVF(
2691-
vmCtx pkgctx.VirtualMachineContext,
2692-
vmiCache vmopv1.VirtualMachineImageCache) (vimtypes.VirtualMachineConfigSpec, error) {
2693-
2694-
var ovfConfigMap corev1.ConfigMap
2695-
if err := vs.k8sClient.Get(
2696-
vmCtx,
2697-
ctrlclient.ObjectKey{
2698-
Namespace: vmiCache.Namespace,
2699-
Name: vmiCache.Status.OVF.ConfigMapName,
2700-
},
2701-
&ovfConfigMap); err != nil {
2702-
2703-
return vimtypes.VirtualMachineConfigSpec{}, fmt.Errorf(
2704-
"failed to get ovf configmap: %w, %w",
2705-
err,
2706-
pkgerr.VMICacheNotReadyError{Name: vmiCache.Name})
2707-
}
2708-
2709-
var ovfEnvelope ovf.Envelope
2710-
if err := yaml.Unmarshal(
2711-
[]byte(ovfConfigMap.Data["value"]), &ovfEnvelope); err != nil {
2712-
2713-
return vimtypes.VirtualMachineConfigSpec{},
2714-
fmt.Errorf("failed to unmarshal ovf yaml into envelope: %w", err)
2715-
}
2716-
2717-
configSpec, err := ovfEnvelope.ToConfigSpec()
2718-
if err != nil {
2719-
return vimtypes.VirtualMachineConfigSpec{},
2720-
fmt.Errorf("failed to transform ovf to config spec: %w", err)
2721-
}
2722-
2723-
return configSpec, nil
2724-
}
2725-
27262691
func (vs *vSphereVMProvider) getConfigSpecFromVM(
27272692
vmCtx pkgctx.VirtualMachineContext,
27282693
vmiCache vmopv1.VirtualMachineImageCache,

pkg/providers/vsphere/vmprovider_vm_utils.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,23 @@ import (
1414

1515
"github.com/google/uuid"
1616
"github.com/vmware/govmomi/object"
17+
"github.com/vmware/govmomi/ovf"
1718
vimtypes "github.com/vmware/govmomi/vim25/types"
1819
corev1 "k8s.io/api/core/v1"
1920
apierrors "k8s.io/apimachinery/pkg/api/errors"
2021
"k8s.io/apimachinery/pkg/api/resource"
2122
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2223
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
2324

25+
"sigs.k8s.io/yaml"
26+
2427
vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha5"
2528
"github.com/vmware-tanzu/vm-operator/pkg/conditions"
2629
pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config"
2730
pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context"
31+
pkgerr "github.com/vmware-tanzu/vm-operator/pkg/errors"
2832
"github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/constants"
33+
"github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/virtualmachine"
2934
"github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/vmlifecycle"
3035
"github.com/vmware-tanzu/vm-operator/pkg/util"
3136
"github.com/vmware-tanzu/vm-operator/pkg/util/cloudinit"
@@ -602,6 +607,139 @@ func GetVMClassConfigSpec(
602607
return configSpec, nil
603608
}
604609

610+
// GetVMClassConfigSpecFromClassName retrieves the VM Class config spec from a
611+
// VM Class identified by className and namespace. It returns the config spec
612+
// derived from either the VM Class's ConfigSpec field or from its hardware
613+
// devices.
614+
func GetVMClassConfigSpecFromClassName(
615+
ctx context.Context,
616+
k8sClient ctrlclient.Client,
617+
className, namespace string,
618+
) (vimtypes.VirtualMachineConfigSpec, error) {
619+
620+
// For classless VMs, return empty config spec.
621+
if className == "" {
622+
return vimtypes.VirtualMachineConfigSpec{}, nil
623+
}
624+
625+
vmClass := &vmopv1.VirtualMachineClass{}
626+
vmClassKey := ctrlclient.ObjectKey{
627+
Name: className,
628+
Namespace: namespace,
629+
}
630+
631+
if err := k8sClient.Get(ctx, vmClassKey, vmClass); err != nil {
632+
return vimtypes.VirtualMachineConfigSpec{}, fmt.Errorf(
633+
"failed to get VM Class %s: %w", className, err)
634+
}
635+
636+
// If the VM Class has a ConfigSpec, use the existing function to parse it.
637+
if len(vmClass.Spec.ConfigSpec) > 0 {
638+
return GetVMClassConfigSpec(ctx, vmClass.Spec.ConfigSpec)
639+
}
640+
641+
// Otherwise, create config spec from VM Class devices.
642+
return virtualmachine.ConfigSpecFromVMClassDevices(&vmClass.Spec), nil
643+
}
644+
645+
// GetConfigSpecFromOVFCache retrieves the VM config spec from a
646+
// VirtualMachineImageCache for OVF images.
647+
func GetConfigSpecFromOVFCache(
648+
ctx context.Context,
649+
k8sClient ctrlclient.Client,
650+
vmiCache vmopv1.VirtualMachineImageCache) (
651+
vimtypes.VirtualMachineConfigSpec, error) {
652+
653+
var ovfConfigMap corev1.ConfigMap
654+
if err := k8sClient.Get(
655+
ctx,
656+
ctrlclient.ObjectKey{
657+
Namespace: vmiCache.Namespace,
658+
Name: vmiCache.Status.OVF.ConfigMapName,
659+
},
660+
&ovfConfigMap); err != nil {
661+
662+
return vimtypes.VirtualMachineConfigSpec{}, fmt.Errorf(
663+
"failed to get ovf configmap: %w, %w",
664+
err,
665+
pkgerr.VMICacheNotReadyError{Name: vmiCache.Name})
666+
}
667+
668+
var ovfEnvelope ovf.Envelope
669+
if err := yaml.Unmarshal(
670+
[]byte(ovfConfigMap.Data["value"]), &ovfEnvelope); err != nil {
671+
672+
return vimtypes.VirtualMachineConfigSpec{},
673+
fmt.Errorf("failed to unmarshal ovf yaml into envelope: %w", err)
674+
}
675+
676+
configSpec, err := ovfEnvelope.ToConfigSpec()
677+
if err != nil {
678+
return vimtypes.VirtualMachineConfigSpec{},
679+
fmt.Errorf("failed to transform ovf to config spec: %w", err)
680+
}
681+
682+
return configSpec, nil
683+
}
684+
685+
// GetVirtualMachineImageCache retrieves and validates a
686+
// VirtualMachineImageCache resource. It fetches the cache object using the
687+
// provided itemID and podNamespace, then validates that the hardware is ready
688+
// by checking the HardwareReady condition. For OVF image types, it also
689+
// ensures the OVF status exists and the provider version matches the expected
690+
// itemVersion. Returns the validated cache object or an error if the cache is
691+
// not found or not ready.
692+
func GetVirtualMachineImageCache(
693+
ctx context.Context,
694+
k8sClient ctrlclient.Client,
695+
itemID, itemVersion, imageType, podNamespace string,
696+
) (vmopv1.VirtualMachineImageCache, error) {
697+
698+
var (
699+
vmiCache vmopv1.VirtualMachineImageCache
700+
vmiCacheKey = ctrlclient.ObjectKey{
701+
Namespace: podNamespace,
702+
Name: util.VMIName(itemID),
703+
}
704+
)
705+
706+
// Get the VirtualMachineImageCache resource.
707+
if err := k8sClient.Get(ctx, vmiCacheKey, &vmiCache); err != nil {
708+
return vmopv1.VirtualMachineImageCache{},
709+
fmt.Errorf("failed to get vmi cache object: %w, %w",
710+
err,
711+
pkgerr.VMICacheNotReadyError{Name: vmiCacheKey.Name})
712+
}
713+
714+
// Check if the hardware is ready.
715+
hardwareReadyCondition := conditions.Get(
716+
vmiCache,
717+
vmopv1.VirtualMachineImageCacheConditionHardwareReady)
718+
719+
switch {
720+
case hardwareReadyCondition != nil &&
721+
hardwareReadyCondition.Status == metav1.ConditionFalse:
722+
723+
return vmopv1.VirtualMachineImageCache{},
724+
fmt.Errorf("failed to get hardware: %s: %w",
725+
hardwareReadyCondition.Message,
726+
pkgerr.VMICacheNotReadyError{Name: vmiCache.Name})
727+
728+
case hardwareReadyCondition == nil,
729+
hardwareReadyCondition.Status != metav1.ConditionTrue,
730+
imageType == "ovf" && vmiCache.Status.OVF == nil,
731+
imageType == "ovf" && vmiCache.Status.OVF.ProviderVersion != itemVersion:
732+
733+
return vmopv1.VirtualMachineImageCache{},
734+
pkgerr.VMICacheNotReadyError{
735+
Message: "hardware not ready",
736+
Name: vmiCacheKey.Name,
737+
}
738+
}
739+
740+
return vmiCache, nil
741+
}
742+
605743
// GetAttachedDiskUUIDToPVC returns a map of disk UUID to PVC object for all
606744
// attached disks by checking for volumes that have a PersistentVolumeClaim.
607745
func GetAttachedDiskUUIDToPVC(

0 commit comments

Comments
 (0)