diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 0dd31b19f..e5c4e4209 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -217,8 +217,10 @@ rules: - clustervirtualmachineimages/status verbs: - get + - list - patch - update + - watch - apiGroups: - vmoperator.vmware.com resources: @@ -326,8 +328,10 @@ rules: - virtualmachineimages/status verbs: - get + - list - patch - update + - watch - apiGroups: - vmoperator.vmware.com resources: diff --git a/controllers/contentlibrary/v1alpha1/utils/utils.go b/controllers/contentlibrary/v1alpha1/utils/utils.go index f647c5fda..07883b275 100644 --- a/controllers/contentlibrary/v1alpha1/utils/utils.go +++ b/controllers/contentlibrary/v1alpha1/utils/utils.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 VMware, Inc. All Rights Reserved. +// Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package utils @@ -17,22 +17,24 @@ import ( vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha1" ) -// GetImageFieldNameFromItem returns the Image field name in format of "vmi-" -// by using the same identifier from the given Item name in "clitem-". +// GetImageFieldNameFromItem returns the image field name in the format of +// "vmi-" by using the identifier from the given item name "clitem-". func GetImageFieldNameFromItem(itemName string) (string, error) { if !strings.HasPrefix(itemName, ItemFieldNamePrefix) { - return "", fmt.Errorf("item name doesn't start with %q", ItemFieldNamePrefix) + return "", fmt.Errorf("item name %q doesn't start with %q", + itemName, ItemFieldNamePrefix) } + itemNameSplit := strings.Split(itemName, "-") if len(itemNameSplit) < 2 { - return "", fmt.Errorf("item name doesn't have an identifier after %s-", ItemFieldNamePrefix) + return "", fmt.Errorf("item name %q doesn't contain a uuid", itemName) } uuid := strings.Join(itemNameSplit[1:], "-") return fmt.Sprintf("%s-%s", ImageFieldNamePrefix, uuid), nil } -// IsItemReady returns if the given item conditions contain a Ready condition with status True. +// IsItemReady checks if an item is ready by iterating through its conditions. func IsItemReady(itemConditions imgregv1a1.Conditions) bool { var isReady bool for _, condition := range itemConditions { @@ -45,23 +47,42 @@ func IsItemReady(itemConditions imgregv1a1.Conditions) bool { return isReady } -// GetVMImageSpecStatus returns the VirtualMachineImage Spec and Status fields in an image-registry service enabled cluster. -// It first tries to get the namespace scope VM Image; if not found, it looks up the cluster scope VM Image. -func GetVMImageSpecStatus(ctx context.Context, ctrlClient client.Client, imageName, namespace string) ( - spec *vmopv1.VirtualMachineImageSpec, status *vmopv1.VirtualMachineImageStatus, err error) { - +// GetVMImageSpecStatus retrieves the VirtualMachineImageSpec and +// VirtualMachineImageStatus for a given image name and namespace. +// It first checks if a namespace scope image exists by the resource name, +// and if not, checks if a cluster scope image exists by the resource name. +func GetVMImageSpecStatus( + ctx context.Context, + ctrlClient client.Client, + imageName, namespace string) ( + *vmopv1.VirtualMachineImageSpec, + *vmopv1.VirtualMachineImageStatus, + error) { + // Check if a namespace scope image exists by the resource name. vmi := vmopv1.VirtualMachineImage{} - if err = ctrlClient.Get(ctx, client.ObjectKey{Name: imageName, Namespace: namespace}, &vmi); err == nil { - spec = &vmi.Spec - status = &vmi.Status - } else if apierrors.IsNotFound(err) { - // Namespace scope image is not found. Check if a cluster scope image with this name exists. - cvmi := vmopv1.ClusterVirtualMachineImage{} - if err = ctrlClient.Get(ctx, client.ObjectKey{Name: imageName}, &cvmi); err == nil { - spec = &cvmi.Spec - status = &cvmi.Status - } + key := client.ObjectKey{Name: imageName, Namespace: namespace} + err := ctrlClient.Get(ctx, key, &vmi) + if err == nil { + return &vmi.Spec, &vmi.Status, nil + } + if !apierrors.IsNotFound(err) { + return nil, nil, err + } + + // Check if a cluster scope image exists by the resource name. + cvmi := vmopv1.ClusterVirtualMachineImage{} + key = client.ObjectKey{Name: imageName} + err = ctrlClient.Get(ctx, key, &cvmi) + if err == nil { + return &cvmi.Spec, &cvmi.Status, nil + } + if !apierrors.IsNotFound(err) { + return nil, nil, err } - return spec, status, err + err = fmt.Errorf( + "cannot find a namespace or cluster scope VM image for resource name %q", + imageName, + ) + return nil, nil, err } diff --git a/pkg/lib/env.go b/pkg/lib/env.go index 6730a0ea9..06591194e 100644 --- a/pkg/lib/env.go +++ b/pkg/lib/env.go @@ -14,6 +14,7 @@ import ( const ( TrueString = "true" + FalseString = "false" VmopNamespaceEnv = "POD_NAMESPACE" WcpFaultDomainsFSS = "FSS_WCP_FAULTDOMAINS" VMServiceV1Alpha2FSS = "FSS_WCP_VMSERVICE_V1ALPHA2" diff --git a/pkg/vmprovider/providers/vsphere/vmprovider_vm_utils.go b/pkg/vmprovider/providers/vsphere/vmprovider_vm_utils.go index fb9e791bb..b89b1b930 100644 --- a/pkg/vmprovider/providers/vsphere/vmprovider_vm_utils.go +++ b/pkg/vmprovider/providers/vsphere/vmprovider_vm_utils.go @@ -349,14 +349,13 @@ func resolveVMImage( imageSpec, imageStatus, err := clutils.GetVMImageSpecStatus(vmCtx, k8sClient, imageName, vmCtx.VM.Namespace) if err != nil { - imageNotFoundMsg := fmt.Sprintf("Failed to get the VM's image: %s", imageName) conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachinePrereqReadyCondition, vmopv1.VirtualMachineImageNotFoundReason, vmopv1.ConditionSeverityError, - imageNotFoundMsg, + fmt.Sprintf("Failed to get the VM's image: %s", imageName), ) - return nil, nil, errors.Wrap(err, imageNotFoundMsg) + return nil, nil, err } // Do not return the VM image status if the current image condition is not satisfied. diff --git a/pkg/vmprovider/providers/vsphere/vmprovider_vm_utils_test.go b/pkg/vmprovider/providers/vsphere/vmprovider_vm_utils_test.go index 8d8acde4f..b01db2177 100644 --- a/pkg/vmprovider/providers/vsphere/vmprovider_vm_utils_test.go +++ b/pkg/vmprovider/providers/vsphere/vmprovider_vm_utils_test.go @@ -6,7 +6,7 @@ package vsphere_test import ( goctx "context" "fmt" - "sync/atomic" + "os" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -56,7 +56,6 @@ func vmUtilTests() { }) Context("GetVirtualMachineClass", func() { - oldNamespacedVMClassFSSEnabledFunc := lib.IsNamespacedVMClassFSSEnabled When("WCP_Namespaced_VM_Class FSS is disabled", func() { var ( @@ -68,13 +67,11 @@ func vmUtilTests() { vmClass, vmClassBinding = builder.DummyVirtualMachineClassAndBinding("dummy-vm-class", vmCtx.VM.Namespace) vmCtx.VM.Spec.ClassName = vmClass.Name - lib.IsNamespacedVMClassFSSEnabled = func() bool { - return false - } + Expect(os.Setenv(lib.NamespacedVMClassFSS, lib.FalseString)).To(Succeed()) }) AfterEach(func() { - lib.IsNamespacedVMClassFSSEnabled = oldNamespacedVMClassFSSEnabledFunc + Expect(os.Unsetenv(lib.NamespacedVMClassFSS)).To(Succeed()) }) Context("VirtualMachineClass custom resource doesn't exist", func() { @@ -165,13 +162,11 @@ func vmUtilTests() { vmClass.Namespace = vmCtx.VM.Namespace vmCtx.VM.Spec.ClassName = vmClass.Name - lib.IsNamespacedVMClassFSSEnabled = func() bool { - return true - } + Expect(os.Setenv(lib.NamespacedVMClassFSS, lib.TrueString)).To(Succeed()) }) AfterEach(func() { - lib.IsNamespacedVMClassFSSEnabled = oldNamespacedVMClassFSSEnabledFunc + Expect(os.Unsetenv(lib.NamespacedVMClassFSS)).To(Succeed()) }) Context("VirtualMachineClass custom resource doesn't exist", func() { @@ -233,9 +228,11 @@ func vmUtilTests() { vmCtx.VM.Spec.ImageName = vmImage.Name - lib.IsWCPVMImageRegistryEnabled = func() bool { - return false - } + Expect(os.Setenv(lib.VMImageRegistryFSS, lib.FalseString)).To(Succeed()) + }) + + AfterEach(func() { + Expect(os.Unsetenv(lib.VMImageRegistryFSS)).To(Succeed()) }) When("VirtualMachineImage does not exist", func() { @@ -348,7 +345,6 @@ func vmUtilTests() { }) When("WCPVMImageRegistry FSS is enabled", func() { - var ( cl *imgregv1a1.ContentLibrary nsVMImage *vmopv1.VirtualMachineImage @@ -371,17 +367,23 @@ func vmUtilTests() { Name: clusterCL.Name, } - lib.IsWCPVMImageRegistryEnabled = func() bool { - return true - } + Expect(os.Setenv(lib.VMImageRegistryFSS, lib.TrueString)).To(Succeed()) + }) + + AfterEach(func() { + Expect(os.Unsetenv(lib.VMImageRegistryFSS)).To(Succeed()) }) When("Neither cluster or namespace scoped VM image exists", func() { + BeforeEach(func() { + vmCtx.VM.Spec.ImageName = "non-existing-image" + }) + It("returns error and sets condition", func() { _, _, _, err := vsphere.GetVMImageAndContentLibraryUUID(vmCtx, k8sClient) Expect(err).To(HaveOccurred()) - expectedErrMsg := fmt.Sprintf("Failed to get the VM's image: %s", vmCtx.VM.Spec.ImageName) + expectedErrMsg := fmt.Sprintf("cannot find a namespace or cluster scope VM image for resource name %q", vmCtx.VM.Spec.ImageName) Expect(err.Error()).To(ContainSubstring(expectedErrMsg)) expectedCondition := vmopv1.Conditions{ @@ -389,7 +391,7 @@ func vmUtilTests() { vmopv1.VirtualMachinePrereqReadyCondition, vmopv1.VirtualMachineImageNotFoundReason, vmopv1.ConditionSeverityError, - expectedErrMsg), + fmt.Sprintf("Failed to get the VM's image: %s", vmCtx.VM.Spec.ImageName)), } Expect(vmCtx.VM.Status.Conditions).To(conditions.MatchConditions(expectedCondition)) }) @@ -481,7 +483,7 @@ func vmUtilTests() { }) }) - When("Namespace scoped VirtualMachineImage exists and ready", func() { + When("Namespace scoped VirtualMachineImage is ready and VM specifies an image CR name", func() { BeforeEach(func() { conditions.MarkTrue(nsVMImage, vmopv1.VirtualMachineImageProviderReadyCondition) conditions.MarkTrue(nsVMImage, vmopv1.VirtualMachineImageProviderSecurityComplianceCondition) @@ -490,15 +492,18 @@ func vmUtilTests() { vmCtx.VM.Spec.ImageName = nsVMImage.Name }) - It("returns success", func() { - _, imageStatus, uuid, err := vsphere.GetVMImageAndContentLibraryUUID(vmCtx, k8sClient) + It("returns expected namespace image spec, status, and CL uuid", func() { + imageSpec, imageStatus, uuid, err := vsphere.GetVMImageAndContentLibraryUUID(vmCtx, k8sClient) Expect(err).ToNot(HaveOccurred()) + Expect(imageSpec).ToNot(BeNil()) + Expect(imageSpec.ImageID).To(Equal(nsVMImage.Spec.ImageID)) Expect(imageStatus).ToNot(BeNil()) + Expect(imageStatus.ImageName).To(Equal(nsVMImage.Status.ImageName)) Expect(uuid).To(Equal(string(cl.Spec.UUID))) }) }) - When("ClusterVirtualMachineImage exists and ready", func() { + When("ClusterVirtualMachineImage is ready and VM specifies an image CR name", func() { BeforeEach(func() { conditions.MarkTrue(clusterVMImage, vmopv1.VirtualMachineImageProviderReadyCondition) conditions.MarkTrue(clusterVMImage, vmopv1.VirtualMachineImageProviderSecurityComplianceCondition) @@ -507,10 +512,13 @@ func vmUtilTests() { vmCtx.VM.Spec.ImageName = clusterVMImage.Name }) - It("returns success", func() { - _, imageStatus, uuid, err := vsphere.GetVMImageAndContentLibraryUUID(vmCtx, k8sClient) + It("returns expected cluster image spec, status, and CL uuid", func() { + imageSpec, imageStatus, uuid, err := vsphere.GetVMImageAndContentLibraryUUID(vmCtx, k8sClient) Expect(err).ToNot(HaveOccurred()) + Expect(imageSpec).ToNot(BeNil()) + Expect(imageSpec.ImageID).To(Equal(clusterVMImage.Spec.ImageID)) Expect(imageStatus).ToNot(BeNil()) + Expect(imageStatus.ImageName).To(Equal(clusterVMImage.Status.ImageName)) Expect(uuid).To(Equal(string(clusterCL.Spec.UUID))) }) }) @@ -684,8 +692,7 @@ func vmUtilTests() { Context("AddInstanceStorageVolumes", func() { var ( - vmClass *vmopv1.VirtualMachineClass - instanceStorageFSS uint32 + vmClass *vmopv1.VirtualMachineClass ) expectInstanceStorageVolumes := func( @@ -713,28 +720,29 @@ func vmUtilTests() { } BeforeEach(func() { - lib.IsInstanceStorageFSSEnabled = func() bool { - return atomic.LoadUint32(&instanceStorageFSS) != 0 - } - vmClass = builder.DummyVirtualMachineClass() }) AfterEach(func() { - atomic.StoreUint32(&instanceStorageFSS, 0) + Expect(os.Unsetenv(lib.InstanceStorageFSS)).To(Succeed()) }) - It("Instance Storage FSS is disabled", func() { - atomic.StoreUint32(&instanceStorageFSS, 0) + When("Instance Storage FSS is disabled", func() { - err := vsphere.AddInstanceStorageVolumes(vmCtx, vmClass) - Expect(err).ToNot(HaveOccurred()) - Expect(instancestorage.FilterVolumes(vmCtx.VM)).To(BeEmpty()) + BeforeEach(func() { + Expect(os.Setenv(lib.InstanceStorageFSS, lib.FalseString)).To(Succeed()) + }) + + It("VM Class does not contain instance storage volumes", func() { + err := vsphere.AddInstanceStorageVolumes(vmCtx, vmClass) + Expect(err).ToNot(HaveOccurred()) + Expect(instancestorage.FilterVolumes(vmCtx.VM)).To(BeEmpty()) + }) }) When("InstanceStorage FFS is enabled", func() { BeforeEach(func() { - atomic.StoreUint32(&instanceStorageFSS, 1) + Expect(os.Setenv(lib.InstanceStorageFSS, lib.TrueString)).To(Succeed()) }) It("VM Class does not contain instance storage volumes", func() { diff --git a/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator.go b/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator.go index 49f4b9680..57e369502 100644 --- a/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator.go +++ b/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator.go @@ -38,9 +38,36 @@ const ( // +kubebuilder:webhook:path=/default-mutate-vmoperator-vmware-com-v1alpha1-virtualmachine,mutating=true,failurePolicy=fail,groups=vmoperator.vmware.com,resources=virtualmachines,verbs=create;update,versions=v1alpha1,name=default.mutating.virtualmachine.vmoperator.vmware.com,sideEffects=None,admissionReviewVersions=v1;v1beta1 // +kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachine,verbs=get;list // +kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachine/status,verbs=get +// +kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachineimages,verbs=get;list;watch +// +kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachineimages/status,verbs=get;list;watch +// +kubebuilder:rbac:groups=vmoperator.vmware.com,resources=clustervirtualmachineimages,verbs=get;list;watch +// +kubebuilder:rbac:groups=vmoperator.vmware.com,resources=clustervirtualmachineimages/status,verbs=get;list;watch // AddToManager adds the webhook to the provided manager. func AddToManager(ctx *context.ControllerManagerContext, mgr ctrlmgr.Manager) error { + // Index the VirtualMachineImage and ClusterVirtualMachineImage objects by + // status.imageName field to allow efficient querying in ResolveImageName(). + if err := mgr.GetFieldIndexer().IndexField( + ctx, + &vmopv1.VirtualMachineImage{}, + "status.imageName", + func(rawObj client.Object) []string { + vmi := rawObj.(*vmopv1.VirtualMachineImage) + return []string{vmi.Status.ImageName} + }); err != nil { + return err + } + if err := mgr.GetFieldIndexer().IndexField( + ctx, + &vmopv1.ClusterVirtualMachineImage{}, + "status.imageName", + func(rawObj client.Object) []string { + cvmi := rawObj.(*vmopv1.ClusterVirtualMachineImage) + return []string{cvmi.Status.ImageName} + }); err != nil { + return err + } + hook, err := builder.NewMutatingWebhook(ctx, mgr, webHookName, NewMutator(mgr.GetClient())) if err != nil { return errors.Wrapf(err, "failed to create mutation webhook") @@ -85,6 +112,11 @@ func (m mutator) Mutate(ctx *context.WebhookRequestContext) admission.Response { if SetDefaultPowerState(ctx, m.client, modified) { wasMutated = true } + if mutated, err := ResolveImageName(ctx, m.client, modified); err != nil { + return admission.Denied(err.Error()) + } else if mutated { + wasMutated = true + } case admissionv1.Update: oldVM, err := m.vmFromUnstructured(ctx.OldObj) if err != nil { @@ -154,6 +186,48 @@ func SetDefaultPowerState( return false } +// ResolveImageName mutates the vm.spec.imageName if it's not set to a vmi name +// and there is a single namespace or cluster scope image with that status name. +func ResolveImageName( + ctx *context.WebhookRequestContext, + c client.Client, + vm *vmopv1.VirtualMachine) (bool, error) { + // Return early if the VM image name is empty or already set to a vmi name. + imageName := vm.Spec.ImageName + if imageName == "" || !lib.IsWCPVMImageRegistryEnabled() || + strings.HasPrefix(imageName, "vmi-") { + return false, nil + } + + // Check if a single namespace scope image exists by the status name. + vmiList := &vmopv1.VirtualMachineImageList{} + if err := c.List(ctx, vmiList, client.InNamespace(vm.Namespace), + client.MatchingFields{ + "status.imageName": imageName, + }, + ); err != nil { + return false, err + } + if len(vmiList.Items) == 1 { + vm.Spec.ImageName = vmiList.Items[0].Name + return true, nil + } + + // Check if a single cluster scope image exists by the status name. + cvmiList := &vmopv1.ClusterVirtualMachineImageList{} + if err := c.List(ctx, cvmiList, client.MatchingFields{ + "status.imageName": imageName, + }); err != nil { + return false, err + } + if len(cvmiList.Items) == 1 { + vm.Spec.ImageName = cvmiList.Items[0].Name + return true, nil + } + + return false, errors.Errorf("no single VM image exists for %q", imageName) +} + // SetNextRestartTime sets spec.nextRestartTime for a VM if the field's // current value is equal to "now" (case-insensitive). // Return true if set, otherwise false. diff --git a/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator_intg_test.go b/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator_intg_test.go index b5347e347..215865f06 100644 --- a/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator_intg_test.go +++ b/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator_intg_test.go @@ -170,6 +170,34 @@ func intgTestsMutating() { }) + Context("ResolveImageName", func() { + + BeforeEach(func() { + Expect(os.Setenv(lib.VMImageRegistryFSS, lib.TrueString)).To(Succeed()) + }) + + AfterEach(func() { + Expect(os.Unsetenv(lib.VMImageRegistryFSS)).To(Succeed()) + }) + + When("Creating VirtualMachine", func() { + + When("When VM ImageName is already a vmi resource name", func() { + + BeforeEach(func() { + vm.Spec.ImageName = "vmi-123" + }) + + It("Should not mutate ImageName", func() { + Expect(ctx.Client.Create(ctx, vm)).To(Succeed()) + modified := &vmopv1.VirtualMachine{} + Expect(ctx.Client.Get(ctx, client.ObjectKeyFromObject(vm), modified)).Should(Succeed()) + Expect(modified.Spec.ImageName).Should(Equal("vmi-123")) + }) + }) + }) + }) + Context("SetNextRestartTime", func() { When("create a VM", func() { When("spec.nextRestartTime is empty", func() { @@ -268,6 +296,5 @@ func intgTestsMutating() { }) }) }) - }) } diff --git a/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator_unit_test.go b/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator_unit_test.go index aedb31d27..2a0b937ba 100644 --- a/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator_unit_test.go +++ b/webhooks/virtualmachine/v1alpha1/mutation/virtualmachine_mutator_unit_test.go @@ -17,6 +17,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha1" @@ -245,6 +247,123 @@ func unitTestsMutating() { }) }) + Describe(("ResolveImageName"), func() { + + BeforeEach(func() { + // Replace the client with a fake client that has the index of VM images. + ctx.Client = fake.NewClientBuilder().WithScheme(builder.NewScheme()). + WithIndex( + &vmopv1.VirtualMachineImage{}, + "status.imageName", + func(rawObj client.Object) []string { + image := rawObj.(*vmopv1.VirtualMachineImage) + return []string{image.Status.ImageName} + }). + WithIndex(&vmopv1.ClusterVirtualMachineImage{}, + "status.imageName", + func(rawObj client.Object) []string { + image := rawObj.(*vmopv1.ClusterVirtualMachineImage) + return []string{image.Status.ImageName} + }).Build() + Expect(os.Setenv(lib.VMImageRegistryFSS, lib.TrueString)).To(Succeed()) + }) + + AfterEach(func() { + Expect(os.Unsetenv(lib.VMImageRegistryFSS)).To(Succeed()) + }) + + Context("When VM ImageName is set to vmi resource name", func() { + + BeforeEach(func() { + ctx.vm.Spec.ImageName = "vmi-xxx" + }) + + It("Should not mutate ImageName", func() { + oldVM := ctx.vm.DeepCopy() + mutated, err := mutation.ResolveImageName(&ctx.WebhookRequestContext, ctx.Client, ctx.vm) + Expect(err).ToNot(HaveOccurred()) + Expect(mutated).To(BeFalse()) + Expect(ctx.vm.Spec.ImageName).Should(Equal(oldVM.Spec.ImageName)) + }) + }) + + Context("When VM ImageName is set to a status name matching multiple namespace scope images", func() { + + BeforeEach(func() { + dupStatusName := "dup-status-name" + vmi1 := builder.DummyVirtualMachineImage("vmi-1") + vmi1.Status.ImageName = dupStatusName + vmi2 := builder.DummyVirtualMachineImage("vmi-2") + vmi2.Status.ImageName = dupStatusName + Expect(ctx.Client.Create(ctx, vmi1)).To(Succeed()) + Expect(ctx.Client.Create(ctx, vmi2)).To(Succeed()) + ctx.vm.Spec.ImageName = dupStatusName + }) + + It("Should return an error", func() { + _, err := mutation.ResolveImageName(&ctx.WebhookRequestContext, ctx.Client, ctx.vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("no single VM image exists for \"dup-status-name\"")) + }) + }) + + Context("When VM ImageName is set to a status name matching multiple cluster scope images", func() { + + BeforeEach(func() { + dupStatusName := "dup-status-name" + cvmi1 := builder.DummyClusterVirtualMachineImage("cvmi-1") + cvmi1.Status.ImageName = dupStatusName + cvmi2 := builder.DummyClusterVirtualMachineImage("cvmi-2") + cvmi2.Status.ImageName = dupStatusName + Expect(ctx.Client.Create(ctx, cvmi1)).To(Succeed()) + Expect(ctx.Client.Create(ctx, cvmi2)).To(Succeed()) + ctx.vm.Spec.ImageName = dupStatusName + }) + + It("Should return an error", func() { + _, err := mutation.ResolveImageName(&ctx.WebhookRequestContext, ctx.Client, ctx.vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("no single VM image exists for \"dup-status-name\"")) + }) + }) + + Context("When VM ImageName is set to a status name matching a single namespace scope image", func() { + + BeforeEach(func() { + uniqueStatusName := "unique-status-name" + vmi := builder.DummyVirtualMachineImage("vmi-123") + vmi.Status.ImageName = uniqueStatusName + Expect(ctx.Client.Create(ctx, vmi)).To(Succeed()) + ctx.vm.Spec.ImageName = uniqueStatusName + }) + + It("Should mutate ImageName to the resource name of the namespace scope image", func() { + mutated, err := mutation.ResolveImageName(&ctx.WebhookRequestContext, ctx.Client, ctx.vm) + Expect(err).ToNot(HaveOccurred()) + Expect(mutated).To(BeTrue()) + Expect(ctx.vm.Spec.ImageName).Should(Equal("vmi-123")) + }) + }) + + Context("When VM ImageName is set to a status name matching a single cluster scope image", func() { + + BeforeEach(func() { + uniqueStatusName := "unique-status-name" + cvmi := builder.DummyClusterVirtualMachineImage("vmi-123") + cvmi.Status.ImageName = uniqueStatusName + Expect(ctx.Client.Create(ctx, cvmi)).To(Succeed()) + ctx.vm.Spec.ImageName = uniqueStatusName + }) + + It("Should mutate ImageName to the resource name of the cluster scope image", func() { + mutated, err := mutation.ResolveImageName(&ctx.WebhookRequestContext, ctx.Client, ctx.vm) + Expect(err).ToNot(HaveOccurred()) + Expect(mutated).To(BeTrue()) + Expect(ctx.vm.Spec.ImageName).Should(Equal("vmi-123")) + }) + }) + }) + Describe("SetNextRestartTime", func() { var (