From b0b5e208b8fa6e38e12aa6b680000f87a0915625 Mon Sep 17 00:00:00 2001 From: akutz Date: Tue, 7 Jan 2025 11:41:00 -0600 Subject: [PATCH] Fast Deploy Direct This patch adds support for Fast Deploy Direct -- deploying a VM from a copy of the source image, cached per-datastore where VMs are deployed. --- .golangci.yml | 4 + .../virtualmachineimagecache_types.go | 203 ++++ api/v1alpha3/zz_generated.deepcopy.go | 185 ++++ .../zz_virtualmachine_guestosid_generated.go | 2 +- ....vmware.com_virtualmachineimagecaches.yaml | 268 ++++++ config/rbac/role.yaml | 2 + .../contentlibraryitem_controller.go | 183 ++-- controllers/controllers.go | 7 + .../virtualmachine_controller.go | 43 +- ...almachineimagecache_controller_internal.go | 18 + .../virtualmachineimagecache_controller.go | 738 ++++++++++++++ ...machineimagecache_controller_suite_test.go | 24 + ...irtualmachineimagecache_controller_test.go | 909 ++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- pkg/config/config.go | 20 + pkg/config/default.go | 2 + pkg/config/env.go | 2 + pkg/config/env/env.go | 6 + pkg/config/env_test.go | 4 + pkg/constants/constants.go | 20 + pkg/errors/requeue_error.go | 42 + .../content_library_provider.go | 41 + .../contentlibrary/content_library_test.go | 4 +- .../vsphere/placement/zone_placement.go | 3 + .../vsphere/virtualmachine/publish_test.go | 2 +- pkg/providers/vsphere/vmlifecycle/create.go | 13 +- .../vmlifecycle/create_contentlibrary.go | 12 +- .../create_contentlibrary_linked_clone.go | 226 ----- .../vsphere/vmlifecycle/create_fastdeploy.go | 315 ++++++ pkg/providers/vsphere/vmprovider.go | 134 ++- pkg/providers/vsphere/vmprovider_test.go | 416 ++++++-- pkg/providers/vsphere/vmprovider_vm.go | 115 ++- pkg/providers/vsphere/vmprovider_vm_utils.go | 4 + pkg/providers/vsphere/vsphere_suite_test.go | 1 - pkg/util/vmopv1/image.go | 117 ++- pkg/util/vmopv1/image_test.go | 335 +++++++ pkg/util/vsphere/datastore/datastore.go | 138 +++ .../vsphere/datastore/datastore_suite_test.go | 17 + pkg/util/vsphere/datastore/datastore_test.go | 395 ++++++++ pkg/util/vsphere/library/item_cache.go | 123 +-- pkg/util/vsphere/library/item_cache_test.go | 253 +---- pkg/util/vsphere/library/item_sync.go | 58 -- pkg/util/vsphere/library/item_sync_test.go | 171 ---- test/builder/fake.go | 1 + .../testdata/images/ttylinux-pc_i486-16.1.mf | 4 +- .../testdata/images/ttylinux-pc_i486-16.1.ova | Bin 10604032 -> 10604032 bytes .../testdata/images/ttylinux-pc_i486-16.1.ovf | 2 +- test/builder/vcsim_test_context.go | 123 ++- 49 files changed, 4659 insertions(+), 1052 deletions(-) create mode 100644 api/v1alpha3/virtualmachineimagecache_types.go create mode 100644 config/crd/bases/vmoperator.vmware.com_virtualmachineimagecaches.yaml create mode 100644 controllers/virtualmachineimagecache/internal/virtualmachineimagecache_controller_internal.go create mode 100644 controllers/virtualmachineimagecache/virtualmachineimagecache_controller.go create mode 100644 controllers/virtualmachineimagecache/virtualmachineimagecache_controller_suite_test.go create mode 100644 controllers/virtualmachineimagecache/virtualmachineimagecache_controller_test.go create mode 100644 pkg/errors/requeue_error.go delete mode 100644 pkg/providers/vsphere/vmlifecycle/create_contentlibrary_linked_clone.go create mode 100644 pkg/providers/vsphere/vmlifecycle/create_fastdeploy.go create mode 100644 pkg/util/vsphere/datastore/datastore.go create mode 100644 pkg/util/vsphere/datastore/datastore_suite_test.go create mode 100644 pkg/util/vsphere/datastore/datastore_test.go delete mode 100644 pkg/util/vsphere/library/item_sync.go delete mode 100644 pkg/util/vsphere/library/item_sync_test.go diff --git a/.golangci.yml b/.golangci.yml index f62c04fba..6e7384bd2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -90,6 +90,10 @@ linters-settings: pkg: github.com/vmware-tanzu/vm-operator/pkg/util - alias: proberctx pkg: github.com/vmware-tanzu/vm-operator/pkg/prober/context + - alias: dsutil + pkg: github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/datastore + - alias: clsutil + pkg: github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/library depguard: rules: diff --git a/api/v1alpha3/virtualmachineimagecache_types.go b/api/v1alpha3/virtualmachineimagecache_types.go new file mode 100644 index 000000000..478a14ceb --- /dev/null +++ b/api/v1alpha3/virtualmachineimagecache_types.go @@ -0,0 +1,203 @@ +// // © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // VirtualMachineImageCacheConditionProviderReady indicates the underlying + // provider is fully synced, including its disks being available locally on + // some datastore. + VirtualMachineImageCacheConditionProviderReady = "VirtualMachineImageCacheProviderReady" + + // VirtualMachineImageCacheConditionDisksReady indicates the disks are + // cached in a given location. + VirtualMachineImageCacheConditionDisksReady = "VirtualMachineImageCacheDisksReady" + + // VirtualMachineImageCacheConditionOVFReady indicates the OVF is cached. + VirtualMachineImageCacheConditionOVFReady = "VirtualMachineImageCacheOVFReady" +) + +type VirtualMachineImageCacheObjectRef struct { + // Kind is a string value representing the REST resource this object + // represents. + // Servers may infer this from the endpoint the client submits requests to. + // Cannot be updated. + // In CamelCase. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + Kind string `json:"kind"` + + // +optional + + // Namespace refers to a namespace. + // This field will be empty if Kind refers to a cluster-scoped resource. + Namespace string `json:"namespace,omitempty"` + + // Name refers to a unique resource. + // More info: http://kubernetes.io/docs/user-guide/identifiers#names + Name string `json:"name"` +} + +type VirtualMachineImageCacheLocationSpec struct { + + // DatacenterID describes the ID of the datacenter to which the image should + // be cached. + DatacenterID string `json:"datacenterID"` + + // DatastoreID describes the ID of the datastore to which the image should + // be cached. + DatastoreID string `json:"datastoreID"` +} + +// VirtualMachineImageCacheSpec defines the desired state of +// VirtualMachineImageCache. +type VirtualMachineImageCacheSpec struct { + // ProviderID describes the ID of the provider item to which the image + // corresponds. + // If the provider is Content Library, the ID refers to a Content Library + // item. + ProviderID string `json:"providerID"` + + // ProviderVersion describes the version of the provider item to which the + // image corresponds. + // The provider is Content Library, the version is the content version. + ProviderVersion string `json:"providerVersion"` + + // +optional + // +listType=map + // +listMapKey=datacenterID + // +listMapKey=datastoreID + + // Locations describes the locations where the image should be cached. + Locations []VirtualMachineImageCacheLocationSpec `json:"locations,omitempty"` +} + +func (s *VirtualMachineImageCacheSpec) SetLocation(dcID, dsID string) { + for i := range s.Locations { + l := s.Locations[i] + if l.DatacenterID == dcID && l.DatastoreID == dsID { + return + } + } + s.Locations = append(s.Locations, VirtualMachineImageCacheLocationSpec{ + DatacenterID: dcID, + DatastoreID: dsID, + }) +} + +type VirtualMachineImageCacheLocationStatus struct { + + // DatacenterID describes the ID of the datacenter to which the image should + // be cached. + DatacenterID string `json:"datacenterID"` + + // DatastoreID describes the ID of the datastore to which the image should + // be cached. + DatastoreID string `json:"datastoreID"` + + // +optional + + // Files describes the paths to the image's cached files on this datastore. + Files []string `json:"files,omitempty"` + + // +optional + + // Conditions describes any conditions associated with this cache location. + // + // Generally this should just include the ReadyType condition. + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +func (i VirtualMachineImageCacheLocationStatus) GetConditions() []metav1.Condition { + return i.Conditions +} + +func (i *VirtualMachineImageCacheLocationStatus) SetConditions(conditions []metav1.Condition) { + i.Conditions = conditions +} + +type VirtualMachineImageCacheOVFStatus struct { + + // +optional + + // ConfigMapName describes the name of the ConfigMap resource that contains + // the image's OVF envelope encoded as YAML. The data is located in the + // ConfigMap key "value". + ConfigMapName string `json:"configMapName,omitempty"` + + // +optional + + // ProviderVersion describes the observed provider version at which the OVF + // is cached. + // The provider is Content Library, the version is the content version. + ProviderVersion string `json:"providerVersion,omitempty"` +} + +// VirtualMachineImageCacheStatus defines the observed state of +// VirtualMachineImageCache. +type VirtualMachineImageCacheStatus struct { + + // +optional + // +listType=map + // +listMapKey=datacenterID + // +listMapKey=datastoreID + + // Locations describe the observed locations where the image is cached. + Locations []VirtualMachineImageCacheLocationStatus `json:"locations,omitempty"` + + // +optional + + // OVF describes the observed status of the cached OVF content. + OVF *VirtualMachineImageCacheOVFStatus `json:"ovf,omitempty"` + + // +optional + + // Conditions describes any conditions associated with this cached image. + // + // Generally this should just include the ReadyType condition, which will + // only be True if all of the cached locations also have True ReadyType + // condition. + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +func (i VirtualMachineImageCache) GetConditions() []metav1.Condition { + return i.Status.Conditions +} + +func (i *VirtualMachineImageCache) SetConditions(conditions []metav1.Condition) { + i.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced,shortName=vmimagecache +// +kubebuilder:storageversion +// +kubebuilder:subresource:status + +// VirtualMachineImageCache is the schema for the +// virtualmachineimagecaches API. +type VirtualMachineImageCache struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VirtualMachineImageCacheSpec `json:"spec,omitempty"` + Status VirtualMachineImageCacheStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// VirtualMachineImageCacheList contains a list of VirtualMachineImageCache. +type VirtualMachineImageCacheList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VirtualMachineImageCache `json:"items"` +} + +func init() { + objectTypes = append(objectTypes, + &VirtualMachineImageCache{}, + &VirtualMachineImageCacheList{}) +} diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index d76c538cb..38ffd8158 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -820,6 +820,191 @@ func (in *VirtualMachineImage) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineImageCache) DeepCopyInto(out *VirtualMachineImageCache) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineImageCache. +func (in *VirtualMachineImageCache) DeepCopy() *VirtualMachineImageCache { + if in == nil { + return nil + } + out := new(VirtualMachineImageCache) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineImageCache) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineImageCacheList) DeepCopyInto(out *VirtualMachineImageCacheList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VirtualMachineImageCache, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineImageCacheList. +func (in *VirtualMachineImageCacheList) DeepCopy() *VirtualMachineImageCacheList { + if in == nil { + return nil + } + out := new(VirtualMachineImageCacheList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineImageCacheList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineImageCacheLocationSpec) DeepCopyInto(out *VirtualMachineImageCacheLocationSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineImageCacheLocationSpec. +func (in *VirtualMachineImageCacheLocationSpec) DeepCopy() *VirtualMachineImageCacheLocationSpec { + if in == nil { + return nil + } + out := new(VirtualMachineImageCacheLocationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineImageCacheLocationStatus) DeepCopyInto(out *VirtualMachineImageCacheLocationStatus) { + *out = *in + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = make([]string, 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]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineImageCacheLocationStatus. +func (in *VirtualMachineImageCacheLocationStatus) DeepCopy() *VirtualMachineImageCacheLocationStatus { + if in == nil { + return nil + } + out := new(VirtualMachineImageCacheLocationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineImageCacheOVFStatus) DeepCopyInto(out *VirtualMachineImageCacheOVFStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineImageCacheOVFStatus. +func (in *VirtualMachineImageCacheOVFStatus) DeepCopy() *VirtualMachineImageCacheOVFStatus { + if in == nil { + return nil + } + out := new(VirtualMachineImageCacheOVFStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineImageCacheObjectRef) DeepCopyInto(out *VirtualMachineImageCacheObjectRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineImageCacheObjectRef. +func (in *VirtualMachineImageCacheObjectRef) DeepCopy() *VirtualMachineImageCacheObjectRef { + if in == nil { + return nil + } + out := new(VirtualMachineImageCacheObjectRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineImageCacheSpec) DeepCopyInto(out *VirtualMachineImageCacheSpec) { + *out = *in + if in.Locations != nil { + in, out := &in.Locations, &out.Locations + *out = make([]VirtualMachineImageCacheLocationSpec, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineImageCacheSpec. +func (in *VirtualMachineImageCacheSpec) DeepCopy() *VirtualMachineImageCacheSpec { + if in == nil { + return nil + } + out := new(VirtualMachineImageCacheSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineImageCacheStatus) DeepCopyInto(out *VirtualMachineImageCacheStatus) { + *out = *in + if in.Locations != nil { + in, out := &in.Locations, &out.Locations + *out = make([]VirtualMachineImageCacheLocationStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.OVF != nil { + in, out := &in.OVF, &out.OVF + *out = new(VirtualMachineImageCacheOVFStatus) + **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]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineImageCacheStatus. +func (in *VirtualMachineImageCacheStatus) DeepCopy() *VirtualMachineImageCacheStatus { + if in == nil { + return nil + } + out := new(VirtualMachineImageCacheStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualMachineImageDiskInfo) DeepCopyInto(out *VirtualMachineImageDiskInfo) { *out = *in diff --git a/api/v1alpha3/zz_virtualmachine_guestosid_generated.go b/api/v1alpha3/zz_virtualmachine_guestosid_generated.go index 7bbc88019..89d5b91e0 100644 --- a/api/v1alpha3/zz_virtualmachine_guestosid_generated.go +++ b/api/v1alpha3/zz_virtualmachine_guestosid_generated.go @@ -6,5 +6,5 @@ package v1alpha3 -// +kubebuilder:validation:Enum=dosGuest;win31Guest;win95Guest;win98Guest;winMeGuest;winNTGuest;win2000ProGuest;win2000ServGuest;win2000AdvServGuest;winXPHomeGuest;winXPProGuest;winXPPro64Guest;winNetWebGuest;winNetStandardGuest;winNetEnterpriseGuest;winNetDatacenterGuest;winNetBusinessGuest;winNetStandard64Guest;winNetEnterprise64Guest;winLonghornGuest;winLonghorn64Guest;winNetDatacenter64Guest;winVistaGuest;winVista64Guest;windows7Guest;windows7_64Guest;windows7Server64Guest;windows8Guest;windows8_64Guest;windows8Server64Guest;windows9Guest;windows9_64Guest;windows9Server64Guest;windows11_64Guest;windows12_64Guest;windowsHyperVGuest;windows2019srv_64Guest;windows2019srvNext_64Guest;windows2022srvNext_64Guest;freebsdGuest;freebsd64Guest;freebsd11Guest;freebsd11_64Guest;freebsd12Guest;freebsd12_64Guest;freebsd13Guest;freebsd13_64Guest;freebsd14Guest;freebsd14_64Guest;redhatGuest;rhel2Guest;rhel3Guest;rhel3_64Guest;rhel4Guest;rhel4_64Guest;rhel5Guest;rhel5_64Guest;rhel6Guest;rhel6_64Guest;rhel7Guest;rhel7_64Guest;rhel8_64Guest;rhel9_64Guest;centosGuest;centos64Guest;centos6Guest;centos6_64Guest;centos7Guest;centos7_64Guest;centos8_64Guest;centos9_64Guest;oracleLinuxGuest;oracleLinux64Guest;oracleLinux6Guest;oracleLinux6_64Guest;oracleLinux7Guest;oracleLinux7_64Guest;oracleLinux8_64Guest;oracleLinux9_64Guest;suseGuest;suse64Guest;slesGuest;sles64Guest;sles10Guest;sles10_64Guest;sles11Guest;sles11_64Guest;sles12Guest;sles12_64Guest;sles15_64Guest;sles16_64Guest;nld9Guest;oesGuest;sjdsGuest;mandrakeGuest;mandrivaGuest;mandriva64Guest;turboLinuxGuest;turboLinux64Guest;ubuntuGuest;ubuntu64Guest;debian4Guest;debian4_64Guest;debian5Guest;debian5_64Guest;debian6Guest;debian6_64Guest;debian7Guest;debian7_64Guest;debian8Guest;debian8_64Guest;debian9Guest;debian9_64Guest;debian10Guest;debian10_64Guest;debian11Guest;debian11_64Guest;debian12Guest;debian12_64Guest;asianux3Guest;asianux3_64Guest;asianux4Guest;asianux4_64Guest;asianux5_64Guest;asianux7_64Guest;asianux8_64Guest;asianux9_64Guest;opensuseGuest;opensuse64Guest;fedoraGuest;fedora64Guest;coreos64Guest;vmwarePhoton64Guest;other24xLinuxGuest;other26xLinuxGuest;otherLinuxGuest;other3xLinuxGuest;other4xLinuxGuest;other5xLinuxGuest;other6xLinuxGuest;genericLinuxGuest;other24xLinux64Guest;other26xLinux64Guest;other3xLinux64Guest;other4xLinux64Guest;other5xLinux64Guest;other6xLinux64Guest;otherLinux64Guest;solaris6Guest;solaris7Guest;solaris8Guest;solaris9Guest;solaris10Guest;solaris10_64Guest;solaris11_64Guest;os2Guest;eComStationGuest;eComStation2Guest;netware4Guest;netware5Guest;netware6Guest;openServer5Guest;openServer6Guest;unixWare7Guest;darwinGuest;darwin64Guest;darwin10Guest;darwin10_64Guest;darwin11Guest;darwin11_64Guest;darwin12_64Guest;darwin13_64Guest;darwin14_64Guest;darwin15_64Guest;darwin16_64Guest;darwin17_64Guest;darwin18_64Guest;darwin19_64Guest;darwin20_64Guest;darwin21_64Guest;darwin22_64Guest;darwin23_64Guest;vmkernelGuest;vmkernel5Guest;vmkernel6Guest;vmkernel65Guest;vmkernel7Guest;vmkernel8Guest;amazonlinux2_64Guest;amazonlinux3_64Guest;crxPod1Guest;crxSys1Guest;rockylinux_64Guest;almalinux_64Guest;otherGuest;otherGuest64 +// +kubebuilder:validation:Enum=dosGuest;win31Guest;win95Guest;win98Guest;winMeGuest;winNTGuest;win2000ProGuest;win2000ServGuest;win2000AdvServGuest;winXPHomeGuest;winXPProGuest;winXPPro64Guest;winNetWebGuest;winNetStandardGuest;winNetEnterpriseGuest;winNetDatacenterGuest;winNetBusinessGuest;winNetStandard64Guest;winNetEnterprise64Guest;winLonghornGuest;winLonghorn64Guest;winNetDatacenter64Guest;winVistaGuest;winVista64Guest;windows7Guest;windows7_64Guest;windows7Server64Guest;windows8Guest;windows8_64Guest;windows8Server64Guest;windows9Guest;windows9_64Guest;windows9Server64Guest;windows11_64Guest;windows12_64Guest;windowsHyperVGuest;windows2019srv_64Guest;windows2019srvNext_64Guest;windows2022srvNext_64Guest;freebsdGuest;freebsd64Guest;freebsd11Guest;freebsd11_64Guest;freebsd12Guest;freebsd12_64Guest;freebsd13Guest;freebsd13_64Guest;freebsd14Guest;freebsd14_64Guest;freebsd15Guest;freebsd15_64Guest;redhatGuest;rhel2Guest;rhel3Guest;rhel3_64Guest;rhel4Guest;rhel4_64Guest;rhel5Guest;rhel5_64Guest;rhel6Guest;rhel6_64Guest;rhel7Guest;rhel7_64Guest;rhel8_64Guest;rhel9_64Guest;rhel10_64Guest;centosGuest;centos64Guest;centos6Guest;centos6_64Guest;centos7Guest;centos7_64Guest;centos8_64Guest;centos9_64Guest;oracleLinuxGuest;oracleLinux64Guest;oracleLinux6Guest;oracleLinux6_64Guest;oracleLinux7Guest;oracleLinux7_64Guest;oracleLinux8_64Guest;oracleLinux9_64Guest;oracleLinux10_64Guest;suseGuest;suse64Guest;slesGuest;sles64Guest;sles10Guest;sles10_64Guest;sles11Guest;sles11_64Guest;sles12Guest;sles12_64Guest;sles15_64Guest;sles16_64Guest;nld9Guest;oesGuest;sjdsGuest;mandrakeGuest;mandrivaGuest;mandriva64Guest;turboLinuxGuest;turboLinux64Guest;ubuntuGuest;ubuntu64Guest;debian4Guest;debian4_64Guest;debian5Guest;debian5_64Guest;debian6Guest;debian6_64Guest;debian7Guest;debian7_64Guest;debian8Guest;debian8_64Guest;debian9Guest;debian9_64Guest;debian10Guest;debian10_64Guest;debian11Guest;debian11_64Guest;debian12Guest;debian12_64Guest;debian13Guest;debian13_64Guest;asianux3Guest;asianux3_64Guest;asianux4Guest;asianux4_64Guest;asianux5_64Guest;asianux7_64Guest;asianux8_64Guest;asianux9_64Guest;miraclelinux_64Guest;pardus_64Guest;opensuseGuest;opensuse64Guest;fedoraGuest;fedora64Guest;coreos64Guest;vmwarePhoton64Guest;other24xLinuxGuest;other26xLinuxGuest;otherLinuxGuest;other3xLinuxGuest;other4xLinuxGuest;other5xLinuxGuest;other6xLinuxGuest;other7xLinuxGuest;genericLinuxGuest;other24xLinux64Guest;other26xLinux64Guest;other3xLinux64Guest;other4xLinux64Guest;other5xLinux64Guest;other6xLinux64Guest;other7xLinux64Guest;otherLinux64Guest;solaris6Guest;solaris7Guest;solaris8Guest;solaris9Guest;solaris10Guest;solaris10_64Guest;solaris11_64Guest;fusionos_64Guest;prolinux_64Guest;kylinlinux_64Guest;os2Guest;eComStationGuest;eComStation2Guest;netware4Guest;netware5Guest;netware6Guest;openServer5Guest;openServer6Guest;unixWare7Guest;darwinGuest;darwin64Guest;darwin10Guest;darwin10_64Guest;darwin11Guest;darwin11_64Guest;darwin12_64Guest;darwin13_64Guest;darwin14_64Guest;darwin15_64Guest;darwin16_64Guest;darwin17_64Guest;darwin18_64Guest;darwin19_64Guest;darwin20_64Guest;darwin21_64Guest;darwin22_64Guest;darwin23_64Guest;vmkernelGuest;vmkernel5Guest;vmkernel6Guest;vmkernel65Guest;vmkernel7Guest;vmkernel8Guest;vmkernel9Guest;amazonlinux2_64Guest;amazonlinux3_64Guest;crxPod1Guest;crxSys1Guest;rockylinux_64Guest;almalinux_64Guest;otherGuest;otherGuest64 type VirtualMachineGuestOSIdentifier string diff --git a/config/crd/bases/vmoperator.vmware.com_virtualmachineimagecaches.yaml b/config/crd/bases/vmoperator.vmware.com_virtualmachineimagecaches.yaml new file mode 100644 index 000000000..754a969b8 --- /dev/null +++ b/config/crd/bases/vmoperator.vmware.com_virtualmachineimagecaches.yaml @@ -0,0 +1,268 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: virtualmachineimagecaches.vmoperator.vmware.com +spec: + group: vmoperator.vmware.com + names: + kind: VirtualMachineImageCache + listKind: VirtualMachineImageCacheList + plural: virtualmachineimagecaches + shortNames: + - vmimagecache + singular: virtualmachineimagecache + scope: Namespaced + versions: + - name: v1alpha3 + schema: + openAPIV3Schema: + description: |- + VirtualMachineImageCache is the schema for the + virtualmachineimagecaches API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + VirtualMachineImageCacheSpec defines the desired state of + VirtualMachineImageCache. + properties: + locations: + description: Locations describes the locations where the image should + be cached. + items: + properties: + datacenterID: + description: |- + DatacenterID describes the ID of the datacenter to which the image should + be cached. + type: string + datastoreID: + description: |- + DatastoreID describes the ID of the datastore to which the image should + be cached. + type: string + required: + - datacenterID + - datastoreID + type: object + type: array + x-kubernetes-list-map-keys: + - datacenterID + - datastoreID + x-kubernetes-list-type: map + providerID: + description: |- + ProviderID describes the ID of the provider item to which the image + corresponds. + If the provider is Content Library, the ID refers to a Content Library + item. + type: string + providerVersion: + description: |- + ProviderVersion describes the version of the provider item to which the + image corresponds. + The provider is Content Library, the version is the content version. + type: string + required: + - providerID + - providerVersion + type: object + status: + description: |- + VirtualMachineImageCacheStatus defines the observed state of + VirtualMachineImageCache. + properties: + conditions: + description: |- + Conditions describes any conditions associated with this cached image. + + Generally this should just include the ReadyType condition, which will + only be True if all of the cached locations also have True ReadyType + condition. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + locations: + description: Locations describe the observed locations where the image + is cached. + items: + properties: + conditions: + description: |- + Conditions describes any conditions associated with this cache location. + + Generally this should just include the ReadyType condition. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + datacenterID: + description: |- + DatacenterID describes the ID of the datacenter to which the image should + be cached. + type: string + datastoreID: + description: |- + DatastoreID describes the ID of the datastore to which the image should + be cached. + type: string + files: + description: Files describes the paths to the image's cached + files on this datastore. + items: + type: string + type: array + required: + - datacenterID + - datastoreID + type: object + type: array + x-kubernetes-list-map-keys: + - datacenterID + - datastoreID + x-kubernetes-list-type: map + ovf: + description: OVF describes the observed status of the cached OVF content. + properties: + configMapName: + description: |- + ConfigMapName describes the name of the ConfigMap resource that contains + the image's OVF envelope encoded as YAML. The data is located in the + ConfigMap key "value". + type: string + providerVersion: + description: |- + ProviderVersion describes the observed provider version at which the OVF + is cached. + The provider is Content Library, the version is the content version. + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 28422953b..3704c232b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -210,6 +210,7 @@ rules: resources: - clustervirtualmachineimages - virtualmachineclasses + - virtualmachineimagecaches - virtualmachineimages - virtualmachinepublishrequests - virtualmachines @@ -240,6 +241,7 @@ rules: - vmoperator.vmware.com resources: - virtualmachineclasses/status + - virtualmachineimagecaches/status - virtualmachinepublishrequests/status - virtualmachinereplicasets/status - virtualmachines/status diff --git a/controllers/contentlibrary/contentlibraryitem/contentlibraryitem_controller.go b/controllers/contentlibrary/contentlibraryitem/contentlibraryitem_controller.go index 9d9d950e5..562f50f84 100644 --- a/controllers/contentlibrary/contentlibraryitem/contentlibraryitem_controller.go +++ b/controllers/contentlibrary/contentlibraryitem/contentlibraryitem_controller.go @@ -6,32 +6,37 @@ package contentlibraryitem import ( "context" + "errors" "fmt" "reflect" "strings" + "github.com/go-logr/logr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "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/handler" "sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/go-logr/logr" - imgregv1a1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha1" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" "github.com/vmware-tanzu/vm-operator/api/v1alpha3/common" "github.com/vmware-tanzu/vm-operator/controllers/contentlibrary/utils" - "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcnd "github.com/vmware-tanzu/vm-operator/pkg/conditions" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" "github.com/vmware-tanzu/vm-operator/pkg/metrics" + "github.com/vmware-tanzu/vm-operator/pkg/patch" "github.com/vmware-tanzu/vm-operator/pkg/providers" + "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" "github.com/vmware-tanzu/vm-operator/pkg/record" imgutil "github.com/vmware-tanzu/vm-operator/pkg/util/image" "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + vmopv1util "github.com/vmware-tanzu/vm-operator/pkg/util/vmopv1" ) // AddToManager adds this package's controller to the provided manager. @@ -52,12 +57,25 @@ func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) err ctx.VMProvider, ) - return ctrl.NewControllerManagedBy(mgr). + builder := ctrl.NewControllerManagedBy(mgr). For(clItemType). // We do not set Owns(VirtualMachineImage) here as we call SetControllerReference() // when creating such resources in the reconciling process below. - WithOptions(controller.Options{MaxConcurrentReconciles: ctx.MaxConcurrentReconciles}). - Complete(r) + WithOptions(controller.Options{MaxConcurrentReconciles: ctx.MaxConcurrentReconciles}) + + if pkgcfg.FromContext(ctx).Features.FastDeploy { + builder = builder.Watches( + &vmopv1.VirtualMachineImageCache{}, + handler.EnqueueRequestsFromMapFunc( + vmopv1util.VirtualMachineImageCacheToItemMapper( + ctx, + r.Client, + imgregv1a1.GroupVersion, + clItemTypeName), + )) + } + + return builder.Complete(r) } func NewReconciler( @@ -100,52 +118,61 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re logger := r.Logger.WithValues("clItemName", req.Name, "namespace", req.Namespace) logger.Info("Reconciling ContentLibraryItem") - clItem := &imgregv1a1.ContentLibraryItem{} - if err := r.Get(ctx, req.NamespacedName, clItem); err != nil { + var obj imgregv1a1.ContentLibraryItem + if err := r.Get(ctx, req.NamespacedName, &obj); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - vmiName, nameErr := utils.GetImageFieldNameFromItem(clItem.Name) + vmiName, nameErr := utils.GetImageFieldNameFromItem(obj.Name) if nameErr != nil { logger.Error(nameErr, "Unsupported ContentLibraryItem name, skip reconciling") return ctrl.Result{}, nil } logger = logger.WithValues("vmiName", vmiName) + patchHelper, err := patch.NewHelper(&obj, r.Client) + if err != nil { + return ctrl.Result{}, fmt.Errorf( + "failed to init patch helper for %s: %w", req.NamespacedName, err) + } + defer func() { + if err := patchHelper.Patch(ctx, &obj); err != nil { + if reterr == nil { + reterr = err + } + logger.Error(err, "patch failed") + } + }() + clItemCtx := &pkgctx.ContentLibraryItemContext{ Context: ctx, Logger: logger, - CLItem: clItem, + CLItem: &obj, ImageObjName: vmiName, } - if !clItem.DeletionTimestamp.IsZero() { + if !obj.DeletionTimestamp.IsZero() { err := r.ReconcileDelete(clItemCtx) return ctrl.Result{}, err } // Create or update the VirtualMachineImage resource accordingly. - err := r.ReconcileNormal(clItemCtx) - return ctrl.Result{}, err + return ctrl.Result{}, r.ReconcileNormal(clItemCtx) } // ReconcileDelete reconciles a deletion for a ContentLibraryItem resource. func (r *Reconciler) ReconcileDelete(ctx *pkgctx.ContentLibraryItemContext) error { - if controllerutil.ContainsFinalizer(ctx.CLItem, utils.CLItemFinalizer) || - controllerutil.ContainsFinalizer(ctx.CLItem, utils.DeprecatedCLItemFinalizer) { - - r.Metrics.DeleteMetrics(ctx.Logger, ctx.ImageObjName, ctx.CLItem.Namespace) - objPatch := client.MergeFromWithOptions( - ctx.CLItem.DeepCopy(), - client.MergeFromWithOptimisticLock{}) + if !controllerutil.ContainsFinalizer(ctx.CLItem, utils.CLItemFinalizer) && + !controllerutil.ContainsFinalizer(ctx.CLItem, utils.DeprecatedCLItemFinalizer) { - controllerutil.RemoveFinalizer(ctx.CLItem, utils.CLItemFinalizer) - controllerutil.RemoveFinalizer(ctx.CLItem, utils.DeprecatedCLItemFinalizer) - - return r.Patch(ctx, ctx.CLItem, objPatch) + return nil } + r.Metrics.DeleteMetrics(ctx.Logger, ctx.ImageObjName, ctx.CLItem.Namespace) + controllerutil.RemoveFinalizer(ctx.CLItem, utils.CLItemFinalizer) + controllerutil.RemoveFinalizer(ctx.CLItem, utils.DeprecatedCLItemFinalizer) + return nil } @@ -155,22 +182,25 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.ContentLibraryItemContext) erro if !controllerutil.ContainsFinalizer(ctx.CLItem, utils.CLItemFinalizer) { // If the object has the deprecated finalizer, remove it. - if updated := controllerutil.RemoveFinalizer(ctx.CLItem, utils.DeprecatedCLItemFinalizer); updated { - ctx.Logger.V(5).Info("Removed deprecated finalizer", "finalizerName", utils.DeprecatedCLItemFinalizer) - } + if controllerutil.RemoveFinalizer( + ctx.CLItem, + utils.DeprecatedCLItemFinalizer) { - objPatch := client.MergeFromWithOptions( - ctx.CLItem.DeepCopy(), - client.MergeFromWithOptimisticLock{}) + ctx.Logger.V(5).Info( + "Removed deprecated finalizer", + "finalizerName", utils.DeprecatedCLItemFinalizer) + } - // The finalizer must be present before proceeding in order to ensure ReconcileDelete() will be called. - // Return immediately after here to update the object and then we'll proceed on the next reconciliation. + // The finalizer must be present before proceeding in order to ensure + // ReconcileDelete() will be called. Return immediately after here to + // update the object and then we'll proceed on the next reconciliation. controllerutil.AddFinalizer(ctx.CLItem, utils.CLItemFinalizer) - return r.Patch(ctx, ctx.CLItem, objPatch) + return nil } - // Do not set additional fields here as they will be overwritten in CreateOrPatch below. + // Do not set additional fields here as they will be overwritten in + // CreateOrPatch below. vmi := &vmopv1.VirtualMachineImage{ ObjectMeta: metav1.ObjectMeta{ Name: ctx.ImageObjName, @@ -179,9 +209,11 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.ContentLibraryItemContext) erro } ctx.VMI = vmi - var didSync bool - var syncErr error - var savedStatus *vmopv1.VirtualMachineImageStatus + var ( + didSync bool + syncErr error + savedStatus *vmopv1.VirtualMachineImageStatus + ) opRes, createOrPatchErr := controllerutil.CreateOrPatch(ctx, r.Client, vmi, func() error { defer func() { @@ -195,7 +227,7 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.ContentLibraryItemContext) erro // Update image condition based on the security compliance of the provider item. clItemSecurityCompliance := ctx.CLItem.Status.SecurityCompliance if clItemSecurityCompliance == nil || !*clItemSecurityCompliance { - conditions.MarkFalse(vmi, + pkgcnd.MarkFalse(vmi, vmopv1.ReadyConditionType, vmopv1.VirtualMachineImageProviderSecurityNotCompliantReason, "Provider item is not security compliant", @@ -207,7 +239,7 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.ContentLibraryItemContext) erro // Check if the item is ready and skip the image content sync if not. if !utils.IsItemReady(ctx.CLItem.Status.Conditions) { - conditions.MarkFalse(vmi, + pkgcnd.MarkFalse(vmi, vmopv1.ReadyConditionType, vmopv1.VirtualMachineImageProviderNotReadyReason, "Provider item is not in ready condition", @@ -216,12 +248,11 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.ContentLibraryItemContext) erro return nil } - syncErr = r.syncImageContent(ctx) - if syncErr == nil { - // In this block, we have confirmed that all the three sub-conditions constituting this - // Ready condition are true, hence mark it as true. - conditions.MarkTrue(vmi, vmopv1.ReadyConditionType) + // If the sync is successful then the VMI resource is ready. + if syncErr = r.syncImageContent(ctx); syncErr == nil { + pkgcnd.MarkTrue(vmi, vmopv1.ReadyConditionType) } + didSync = true // Do not return syncErr here as we still want to patch the updated fields we get above. @@ -262,42 +293,68 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.ContentLibraryItemContext) erro // setUpVMIFromCLItem sets up the VirtualMachineImage fields that // are retrievable from the given ContentLibraryItem resource. func (r *Reconciler) setUpVMIFromCLItem(ctx *pkgctx.ContentLibraryItemContext) error { - clItem := ctx.CLItem - vmi := ctx.VMI - if err := controllerutil.SetControllerReference(clItem, vmi, r.Scheme()); err != nil { + if err := controllerutil.SetControllerReference( + ctx.CLItem, + ctx.VMI, r.Scheme()); err != nil { + return err } - vmi.Spec.ProviderRef = &common.LocalObjectRef{ - APIVersion: clItem.APIVersion, - Kind: clItem.Kind, - Name: clItem.Name, + ctx.VMI.Spec.ProviderRef = &common.LocalObjectRef{ + APIVersion: ctx.CLItem.APIVersion, + Kind: ctx.CLItem.Kind, + Name: ctx.CLItem.Name, } - vmi.Status.Name = clItem.Status.Name - vmi.Status.ProviderItemID = string(clItem.Spec.UUID) - vmi.Status.Type = string(clItem.Status.Type) - return utils.AddContentLibraryRefToAnnotation(vmi, ctx.CLItem.Status.ContentLibraryRef) + ctx.VMI.Status.Name = ctx.CLItem.Status.Name + ctx.VMI.Status.ProviderItemID = string(ctx.CLItem.Spec.UUID) + ctx.VMI.Status.Type = string(ctx.CLItem.Status.Type) + + return utils.AddContentLibraryRefToAnnotation( + ctx.VMI, + ctx.CLItem.Status.ContentLibraryRef) } // syncImageContent syncs the VirtualMachineImage content from the provider. // It skips syncing if the image content is already up-to-date. func (r *Reconciler) syncImageContent(ctx *pkgctx.ContentLibraryItemContext) error { - clItem := ctx.CLItem - vmi := ctx.VMI - latestVersion := clItem.Status.ContentVersion + + var ( + cli = ctx.CLItem + vmi = ctx.VMI + latestVersion = cli.Status.ContentVersion + ) + if vmi.Status.ProviderContentVersion == latestVersion { return nil } - err := r.VMProvider.SyncVirtualMachineImage(ctx, clItem, vmi) + err := r.VMProvider.SyncVirtualMachineImage(ctx, cli, vmi) if err != nil { - conditions.MarkFalse(vmi, - vmopv1.ReadyConditionType, - vmopv1.VirtualMachineImageNotSyncedReason, - "Failed to sync to the latest content version from provider") + if pkgcfg.FromContext(ctx).Features.FastDeploy && + errors.Is(err, vsphere.ErrSyncVMICacheNotReady) { + + // If the VMICache object does not yet have the OVF data, then add a + // label to this CLI resource so that it will be enqueued again once the + // VMICache object is ready due to the request mapper. + if ctx.CLItem.Labels == nil { + ctx.CLItem.Labels = map[string]string{} + } + ctx.CLItem.Labels[pkgconst.VMICacheLabelKey] = vmi.Name + } else { + pkgcnd.MarkFalse(vmi, + vmopv1.ReadyConditionType, + vmopv1.VirtualMachineImageNotSyncedReason, + "Failed to sync to the latest content version from provider") + } } else { vmi.Status.ProviderContentVersion = latestVersion + + if pkgcfg.FromContext(ctx).Features.FastDeploy { + // Now that the VMI is synced, the CLI no longer needs to be reconciled + // when the VMI Cache object is updated. + delete(ctx.CLItem.Labels, pkgconst.VMICacheLabelKey) + } } // Sync the image's type, OS information and capabilities to the resource's diff --git a/controllers/controllers.go b/controllers/controllers.go index b3a8f27bf..7bb504e4b 100644 --- a/controllers/controllers.go +++ b/controllers/controllers.go @@ -15,6 +15,7 @@ import ( spq "github.com/vmware-tanzu/vm-operator/controllers/storagepolicyquota" "github.com/vmware-tanzu/vm-operator/controllers/virtualmachine" "github.com/vmware-tanzu/vm-operator/controllers/virtualmachineclass" + "github.com/vmware-tanzu/vm-operator/controllers/virtualmachineimagecache" "github.com/vmware-tanzu/vm-operator/controllers/virtualmachinepublishrequest" "github.com/vmware-tanzu/vm-operator/controllers/virtualmachinereplicaset" "github.com/vmware-tanzu/vm-operator/controllers/virtualmachineservice" @@ -68,5 +69,11 @@ func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) err } } + if pkgcfg.FromContext(ctx).Features.FastDeploy { + if err := virtualmachineimagecache.AddToManager(ctx, mgr); err != nil { + return fmt.Errorf("failed to initialize VMI controllers: %w", err) + } + } + return nil } diff --git a/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go b/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go index f22e8df01..16d0bab7f 100644 --- a/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go +++ b/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "reflect" - "strconv" "strings" "time" @@ -31,8 +30,10 @@ import ( byokv1 "github.com/vmware-tanzu/vm-operator/external/byok/api/v1alpha1" "github.com/vmware-tanzu/vm-operator/pkg/conditions" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" + pkgerrs "github.com/vmware-tanzu/vm-operator/pkg/errors" "github.com/vmware-tanzu/vm-operator/pkg/metrics" "github.com/vmware-tanzu/vm-operator/pkg/patch" "github.com/vmware-tanzu/vm-operator/pkg/prober" @@ -136,6 +137,18 @@ func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) err } } + if pkgcfg.FromContext(ctx).Features.FastDeploy { + builder = builder.Watches( + &vmopv1.VirtualMachineImageCache{}, + handler.EnqueueRequestsFromMapFunc( + vmopv1util.VirtualMachineImageCacheToItemMapper( + ctx, + r.Client, + vmopv1.GroupVersion, + controlledTypeName), + )) + } + return builder.Complete(r) } @@ -269,6 +282,7 @@ type Reconciler struct { // +kubebuilder:rbac:groups="",resources=resourcequotas;namespaces,verbs=get;list;watch // +kubebuilder:rbac:groups=encryption.vmware.com,resources=encryptionclasses,verbs=get;list;watch +// Reconcile the object. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { ctx = pkgcfg.JoinContext(ctx, r.Context) @@ -293,10 +307,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re logger := ctrl.Log.WithName("VirtualMachine").WithValues("name", vm.NamespacedName()) if pkgcfg.FromContext(ctx).Features.FastDeploy { - // Allow the use of an annotation to control whether fast-deploy is used - // per-VM to deploy the VM. - if val := vm.Annotations["vmoperator.vmware.com/fast-deploy"]; val != "" { - if ok, _ := strconv.ParseBool(val); !ok { + if v := vm.Annotations[pkgconst.FastDeployAnnotationKey]; v != "" { + // Allow the use of an annotation to control whether fast-deploy is + // used per-VM to deploy the VM. + if strings.EqualFold(v, "disabled") || strings.EqualFold(v, "false") { // Create a copy of the config so the feature-state for // FastDeploy can also be influenced by a VM annotation. cfg := pkgcfg.FromContext(ctx) @@ -334,11 +348,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re }() if !vm.DeletionTimestamp.IsZero() { - err = r.ReconcileDelete(vmCtx) - return ctrl.Result{}, err + return pkgerrs.ResultFromError(r.ReconcileDelete(vmCtx)) } - if err = r.ReconcileNormal(vmCtx); err != nil && !ignoredCreateErr(err) { + if err := r.ReconcileNormal(vmCtx); err != nil && !ignoredCreateErr(err) { + if result, err := pkgerrs.ResultFromError(err); err == nil { + return result, nil + } vmCtx.Logger.Error(err, "Failed to reconcile VirtualMachine") return ctrl.Result{}, err } @@ -434,7 +450,7 @@ func (r *Reconciler) ReconcileDelete(ctx *pkgctx.VirtualMachineContext) (reterr } // ReconcileNormal processes a level trigger for this VM: create if it doesn't exist otherwise update the existing VM. -func (r *Reconciler) ReconcileNormal(ctx *pkgctx.VirtualMachineContext) (reterr error) { +func (r *Reconciler) ReconcileNormal(ctx *pkgctx.VirtualMachineContext) (reterr error) { //nolint:gocyclo // Return early if the VM reconciliation is paused. if _, exists := ctx.VM.Annotations[vmopv1.PauseAnnotation]; exists { ctx.Logger.Info("Skipping reconciliation since VirtualMachine contains the pause annotation") @@ -507,6 +523,11 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.VirtualMachineContext) (reterr // // Blocking create // + if err == nil { + // Make sure the VM is no longer reconciled when the VMI + // Cache resource is updated. + delete(ctx.VM.Labels, pkgconst.VMICacheLabelKey) + } r.Recorder.EmitEvent(ctx.VM, "Create", err, false) } else { // @@ -527,6 +548,10 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.VirtualMachineContext) (reterr } if !failed { // If no error the channel is just closed. + + // Make sure the VM is no longer reconciled when the VMI + // Cache resource is updated. + delete(ctx.VM.Labels, pkgconst.VMICacheLabelKey) r.Recorder.EmitEvent(obj, "Create", nil, false) } }(ctx.VM.DeepCopy()) diff --git a/controllers/virtualmachineimagecache/internal/virtualmachineimagecache_controller_internal.go b/controllers/virtualmachineimagecache/internal/virtualmachineimagecache_controller_internal.go new file mode 100644 index 000000000..055417203 --- /dev/null +++ b/controllers/virtualmachineimagecache/internal/virtualmachineimagecache_controller_internal.go @@ -0,0 +1,18 @@ +// // © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package internal + +type contextKey uint8 + +const ( + // NewCacheStorageURIsClientContextKey is used for testing. + NewCacheStorageURIsClientContextKey contextKey = iota + + // NewContentLibraryProviderContextKey is used for testing. + NewContentLibraryProviderContextKey + + // NewTopLevelDirectoryClientContextKey is used for testing. + NewTopLevelDirectoryClientContextKey +) diff --git a/controllers/virtualmachineimagecache/virtualmachineimagecache_controller.go b/controllers/virtualmachineimagecache/virtualmachineimagecache_controller.go new file mode 100644 index 000000000..4a5802bbc --- /dev/null +++ b/controllers/virtualmachineimagecache/virtualmachineimagecache_controller.go @@ -0,0 +1,738 @@ +// // © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package virtualmachineimagecache + +import ( + "context" + "errors" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/go-logr/logr" + "github.com/vmware/govmomi/fault" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/property" + "github.com/vmware/govmomi/vapi/rest" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + ctrlclient "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/manager" + "sigs.k8s.io/yaml" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + "github.com/vmware-tanzu/vm-operator/controllers/virtualmachineimagecache/internal" + pkgcond "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" + pkgerrs "github.com/vmware-tanzu/vm-operator/pkg/errors" + "github.com/vmware-tanzu/vm-operator/pkg/patch" + "github.com/vmware-tanzu/vm-operator/pkg/providers" + clprov "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/contentlibrary" + "github.com/vmware-tanzu/vm-operator/pkg/record" + "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/client" + dsutil "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/datastore" + clsutil "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/library" +) + +// SkipNameValidation is used for testing to allow multiple controllers with the +// same name since Controller-Runtime has a global singleton registry to +// prevent controllers with the same name, even if attached to different +// managers. +var SkipNameValidation *bool + +// AddToManager adds this package's controller to the provided manager. +func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) error { + var ( + controlledType = &vmopv1.VirtualMachineImageCache{} + controlledTypeName = reflect.TypeOf(controlledType).Elem().Name() + + controllerNameShort = fmt.Sprintf("%s-controller", strings.ToLower(controlledTypeName)) + controllerNameLong = fmt.Sprintf("%s/%s/%s", ctx.Namespace, ctx.Name, controllerNameShort) + ) + + r := &reconciler{ + Context: ctx, + Client: mgr.GetClient(), + Logger: ctx.Logger.WithName("controllers").WithName(controlledTypeName), + Recorder: record.New(mgr.GetEventRecorderFor(controllerNameLong)), + VMProvider: ctx.VMProvider, + + newCLSProvdrFn: newContentLibraryProviderOrDefault(ctx), + newSRIClientFn: newCacheStorageURIsClientOrDefault(ctx), + newTLDClientFn: newTopLevelDirectoryClientOrDefault(ctx), + } + + return ctrl.NewControllerManagedBy(mgr). + For(controlledType). + WithOptions(controller.Options{ + SkipNameValidation: SkipNameValidation, + }). + Complete(r) +} + +// reconciler reconciles a VirtualMachineImageCache object. +type reconciler struct { + ctrlclient.Client + Context context.Context + Logger logr.Logger + Recorder record.Recorder + VMProvider providers.VirtualMachineProviderInterface + + newCLSProvdrFn newContentLibraryProviderFn + newSRIClientFn newCacheStorageURIsClientFn + newTLDClientFn newTopLevelDirectoryClientFn +} + +// +kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachineimagecaches,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=vmoperator.vmware.com,resources=virtualmachineimagecaches/status,verbs=get;update;patch + +func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + ctx = pkgcfg.JoinContext(ctx, r.Context) + + var obj vmopv1.VirtualMachineImageCache + if err := r.Get(ctx, req.NamespacedName, &obj); err != nil { + return ctrl.Result{}, ctrlclient.IgnoreNotFound(err) + } + + logger := r.Logger. + WithName("VirtualMachineImageCache"). + WithValues("name", req.NamespacedName) + + ctx = logr.NewContext(ctx, logger) + + patchHelper, err := patch.NewHelper(&obj, r.Client) + if err != nil { + return ctrl.Result{}, fmt.Errorf( + "failed to init patch helper for %s: %w", req.NamespacedName, err) + } + defer func() { + if err := patchHelper.Patch(ctx, &obj); err != nil { + if reterr == nil { + reterr = err + } + logger.Error(err, "patch failed") + } + }() + + if !obj.DeletionTimestamp.IsZero() { + // Noop. + return ctrl.Result{}, nil + } + + return pkgerrs.ResultFromError(r.ReconcileNormal(ctx, &obj)) +} + +const conditionReasonFailed = "Failed" + +var endsWithVMDK = regexp.MustCompile(`(?i)^.+\.vmdk$`) + +func (r *reconciler) ReconcileNormal( + ctx context.Context, + obj *vmopv1.VirtualMachineImageCache) (retErr error) { + + // Reset the version status so it is constructed from scratch each time. + obj.Status = vmopv1.VirtualMachineImageCacheStatus{} + + // If the reconcile failed with an error, then make sure it is reflected in + // the object's Ready condition. + defer func() { + if retErr != nil { + pkgcond.MarkFalse( + obj, + vmopv1.ReadyConditionType, + conditionReasonFailed, + retErr.Error()) + } + }() + + // Verify the item's ID. + if obj.Spec.ProviderID == "" { + return errors.New("spec.providerID is empty") + } + + // Verify the item's version. + if obj.Spec.ProviderVersion == "" { + return errors.New("spec.providerVersion is empty") + } + + // Get a vSphere client. + c, err := r.VMProvider.VSphereClient(ctx) + if err != nil { + return fmt.Errorf("failed to get vSphere client: %w", err) + } + + // Get the content library provider. + clProv := r.newCLSProvdrFn(ctx, c.RestClient()) + + // Reconcile the OVF envelope. + if err := reconcileOVF(ctx, r.Client, clProv, obj); err != nil { + pkgcond.MarkFalse( + obj, + vmopv1.VirtualMachineImageCacheConditionOVFReady, + conditionReasonFailed, + err.Error()) + } else { + pkgcond.MarkTrue( + obj, + vmopv1.VirtualMachineImageCacheConditionOVFReady) + } + + // Reconcile the underlying library item. + if err := reconcileLibraryItem(ctx, clProv, obj); err != nil { + pkgcond.MarkFalse( + obj, + vmopv1.VirtualMachineImageCacheConditionProviderReady, + conditionReasonFailed, + err.Error()) + } else { + pkgcond.MarkTrue( + obj, + vmopv1.VirtualMachineImageCacheConditionProviderReady) + } + + // Reconcile the disks. + if err := r.reconcileDisks(ctx, c, clProv, obj); err != nil { + pkgcond.MarkFalse( + obj, + vmopv1.VirtualMachineImageCacheConditionDisksReady, + conditionReasonFailed, + err.Error()) + } else if len(obj.Spec.Locations) > 0 { + // Aggregate each location's Ready condition into the top-level + // VirtualMachineImageCacheConditionDisksReady condition. + getters := make([]pkgcond.Getter, len(obj.Status.Locations)) + for i := range obj.Status.Locations { + getters[i] = obj.Status.Locations[i] + } + pkgcond.SetAggregate( + obj, + vmopv1.VirtualMachineImageCacheConditionDisksReady, + getters, + pkgcond.WithStepCounter()) + } + + // Create the object's Ready condition based on its other conditions. + pkgcond.SetSummary(obj, pkgcond.WithStepCounter()) + + return nil +} + +func (r *reconciler) reconcileDisks( + ctx context.Context, + vcClient *client.Client, + clProv clprov.Provider, + obj *vmopv1.VirtualMachineImageCache) error { + + if len(obj.Spec.Locations) == 0 { + // There is no point in proceeding if there are no known locations to + // which the disks should be cached. + return nil + } + + var ( + srcDatacenter = vcClient.Datacenter() + vimClient = vcClient.VimClient() + ) + + // Get the library item's storage paths. + srcDiskURIs, err := getSourceDiskPaths( + ctx, + clProv, + srcDatacenter, + obj.Spec.ProviderID) + if err != nil { + return err + } + + // Get the datacenters used by the item. + dstDatacenters, err := getDatacenters(ctx, vimClient, obj) + if err != nil { + return err + } + + // Get the datastores used by the item. + dstDatastores, err := getDatastores(ctx, vimClient, obj) + if err != nil { + return err + } + + // Get the top-level cache directories for each datastore. + dstTopLevelDirs, err := r.getTopLevelCacheDirs( + ctx, + vimClient, + dstDatacenters, + dstDatastores) + if err != nil { + return err + } + + // Reconcile the locations. + r.reconcileLocations( + ctx, + vimClient, + dstDatacenters, + srcDatacenter, + obj, + dstTopLevelDirs, + srcDiskURIs) + + return nil +} + +func (r *reconciler) reconcileLocations( + ctx context.Context, + vimClient *vim25.Client, + dstDatacenters map[string]*object.Datacenter, + srcDatacenter *object.Datacenter, + obj *vmopv1.VirtualMachineImageCache, + dstTopLevelDirs map[string]string, + srcDiskURIs []string) { + + obj.Status.Locations = make( + []vmopv1.VirtualMachineImageCacheLocationStatus, + len(obj.Spec.Locations)) + + for i := range obj.Spec.Locations { + + var ( + spec = obj.Spec.Locations[i] + status = &obj.Status.Locations[i] + conditions = pkgcond.Conditions(status.Conditions) + ) + + status.DatacenterID = spec.DatacenterID + status.DatastoreID = spec.DatastoreID + + files, err := r.cacheDisks( + ctx, + vimClient, + dstDatacenters[spec.DatacenterID], + srcDatacenter, + dstTopLevelDirs[spec.DatastoreID], + obj.Spec.ProviderID, + obj.Spec.ProviderVersion, + srcDiskURIs) + if err != nil { + conditions = conditions.MarkFalse( + vmopv1.ReadyConditionType, + conditionReasonFailed, + err.Error()) + } else { + status.Files = files + conditions = conditions.MarkTrue(vmopv1.ReadyConditionType) + } + + status.Conditions = conditions + } +} + +func (r *reconciler) cacheDisks( + ctx context.Context, + vimClient *vim25.Client, + dstDatacenter, srcDatacenter *object.Datacenter, + tldPath, itemID, itemVersion string, + srcDiskURIs []string) ([]string, error) { + + itemCacheDir := clsutil.GetCacheDirForLibraryItem( + tldPath, + itemID, + itemVersion) + + sriClient := r.newSRIClientFn(vimClient) + + cachedPaths, err := clsutil.CacheStorageURIs( + ctx, + sriClient, + dstDatacenter, + srcDatacenter, + itemCacheDir, + srcDiskURIs...) + if err != nil { + return nil, fmt.Errorf("failed to cache storage items: %w", err) + } + + return cachedPaths, nil +} + +const ( + ovfConfigMapValueKey = "value" + ovfConfigMapContentVersionKey = "contentVersion" +) + +func reconcileOVF( + ctx context.Context, + k8sClient ctrlclient.Client, + clProv clprov.Provider, + obj *vmopv1.VirtualMachineImageCache) error { + + // Ensure the OVF ConfigMap is up-to-date. Please note, this may be a no-op. + configMap := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: obj.Namespace, + Name: obj.Name, + }, + } + if _, err := controllerutil.CreateOrPatch( + ctx, + k8sClient, + &configMap, + func() error { + + if configMap.Data[ovfConfigMapValueKey] != "" && + configMap.Data[ovfConfigMapContentVersionKey] == obj.Spec.ProviderVersion { + // Do nothing if the ConfigMap has the marshaled OVF and it is + // the latest content version. + return nil + } + + if configMap.Data == nil { + configMap.Data = map[string]string{} + } + + // Get the OVF. + ovfEnv, err := clProv.RetrieveOvfEnvelopeByLibraryItemID( + ctx, obj.Spec.ProviderID) + if err != nil { + return fmt.Errorf("failed to retrieve ovf envelope: %w", err) + } + + // Marshal the OVF envelope to YAML. + data, err := yaml.Marshal(ovfEnv) + if err != nil { + return fmt.Errorf("failed to marshal ovf envelope to YAML: %w", err) + } + + configMap.Data[ovfConfigMapContentVersionKey] = obj.Spec.ProviderVersion + configMap.Data[ovfConfigMapValueKey] = string(data) + + return nil + }); err != nil { + + return fmt.Errorf("failed to create or patch ovf configmap: %w", err) + } + + if obj.Status.OVF == nil { + obj.Status.OVF = &vmopv1.VirtualMachineImageCacheOVFStatus{} + } + + obj.Status.OVF.ProviderVersion = obj.Spec.ProviderVersion + obj.Status.OVF.ConfigMapName = configMap.Name + + return nil +} + +func reconcileLibraryItem( + ctx context.Context, + p clprov.Provider, + obj *vmopv1.VirtualMachineImageCache) error { + + // Get the content library item to be cached. + item, err := p.GetLibraryItemID(ctx, obj.Spec.ProviderID) + if err != nil { + return fmt.Errorf("failed to get library item: %w", err) + } + + // If the item is not cached locally, then issue a sync so content library + // fetches the item's disks. + // + // Please note, the m.SyncLibraryItem method is reentrant on the remote + // side. That is to say, if the call gets interrupted and we sync the item + // again while an existing sync is occurring, the client will block until + // the original sync is complete. + if !item.Cached { + if err := p.SyncLibraryItem(ctx, item, true); err != nil { + return fmt.Errorf("failed to sync library item: %w", err) + } + } + + return nil +} + +type datastore struct { + datacenterID string + mo mo.Datastore + obj *object.Datastore +} + +func getSourceDiskPaths( + ctx context.Context, + p clprov.Provider, + datacenter *object.Datacenter, + itemID string) ([]string, error) { + + // Get the storage URIs for the library item's files. + itemStor, err := p.ListLibraryItemStorage(ctx, itemID) + if err != nil { + return nil, fmt.Errorf("failed to list library item storage: %w", err) + } + + // Resolve the item's storage URIs into datastore paths, ex. + // [my-datastore] path/to/file.ext + if err := p.ResolveLibraryItemStorage( + ctx, + datacenter, + itemStor); err != nil { + + return nil, fmt.Errorf("failed to resolve library item storage: %w", err) + } + + // Get the storage URIs for just the files that are disks. + var srcDiskURIs []string + for i := range itemStor { + is := itemStor[i] + for j := range is.StorageURIs { + s := is.StorageURIs[j] + if endsWithVMDK.MatchString(s) { + srcDiskURIs = append(srcDiskURIs, s) + } + } + } + + return srcDiskURIs, nil +} + +func getDatacenters( + ctx context.Context, + vimClient *vim25.Client, + obj *vmopv1.VirtualMachineImageCache) (map[string]*object.Datacenter, error) { + + var ( + refList []vimtypes.ManagedObjectReference + objMap = map[string]*object.Datacenter{} + ) + + // Get a set of unique datacenters used by the item's storage. + for i := range obj.Spec.Locations { + l := obj.Spec.Locations[i] + if _, ok := objMap[l.DatacenterID]; !ok { + ref := vimtypes.ManagedObjectReference{ + Type: "Datacenter", + Value: l.DatacenterID, + } + objMap[l.DatacenterID] = object.NewDatacenter(vimClient, ref) + refList = append(refList, ref) + } + } + + var ( + moList []mo.Datacenter + pc = property.DefaultCollector(vimClient) + ) + + // Populate the properties of the unique datacenters. + if err := pc.Retrieve( + ctx, + refList, + []string{"name"}, + &moList); err != nil { + + if fault.Is(err, &vimtypes.ManagedObjectNotFound{}) { + return nil, errors.New("invalid datacenter ID") + } + + return nil, fmt.Errorf("failed to get datacenter properties: %w", err) + } + + return objMap, nil +} + +func getDatastores( + ctx context.Context, + vimClient *vim25.Client, + obj *vmopv1.VirtualMachineImageCache) (map[string]datastore, error) { + + var ( + refList []vimtypes.ManagedObjectReference + objMap = map[string]datastore{} + ) + + // Get a set of unique datastores used by the item's storage. + for i := range obj.Spec.Locations { + l := obj.Spec.Locations[i] + if _, ok := objMap[l.DatastoreID]; !ok { + ref := vimtypes.ManagedObjectReference{ + Type: "Datastore", + Value: l.DatastoreID, + } + objMap[l.DatastoreID] = datastore{ + datacenterID: l.DatacenterID, + mo: mo.Datastore{ + ManagedEntity: mo.ManagedEntity{ + ExtensibleManagedObject: mo.ExtensibleManagedObject{ + Self: ref, + }, + }, + }, + obj: object.NewDatastore(vimClient, ref), + } + refList = append(refList, ref) + } + } + + var ( + moList []mo.Datastore + pc = property.DefaultCollector(vimClient) + ) + + // Populate the properties of the unique datastores. + if err := pc.Retrieve( + ctx, + refList, + []string{ + "name", + "summary.url", + "capability.topLevelDirectoryCreateSupported", + }, + &moList); err != nil { + + if fault.Is(err, &vimtypes.ManagedObjectNotFound{}) { + return nil, errors.New("invalid datastore ID") + } + + return nil, fmt.Errorf("failed to get datastore properties: %w", err) + } + + for i := range moList { + v := moList[i].Reference().Value + o := objMap[v] + o.mo = moList[i] + objMap[v] = o + } + + return objMap, nil +} + +func (r *reconciler) getTopLevelCacheDirs( + ctx context.Context, + vimClient *vim25.Client, + dstDatacenters map[string]*object.Datacenter, + dstDatastores map[string]datastore) (map[string]string, error) { + + tldClient := r.newTLDClientFn(vimClient) + + // Iterate over the unique datastores and ensure there is a top-level + // cache directory present on each one. + tldMap := map[string]string{} + for k, ds := range dstDatastores { + var topLevelCreateDirSupported bool + if v := ds.mo.Capability.TopLevelDirectoryCreateSupported; v != nil { + topLevelCreateDirSupported = *v + } + + tldPath, err := dsutil.CreateTopLevelDirectory( + ctx, + tldClient, + dstDatacenters[ds.datacenterID], + ds.obj, + ds.mo.Name, + ds.mo.Summary.Url, + topLevelCreateDirSupported, + clsutil.TopLevelCacheDirName) + if err != nil { + return nil, fmt.Errorf( + "failed to create top-level directory: %w", + err) + } + + tldMap[k] = tldPath + } + return tldMap, nil +} + +type newContentLibraryProviderFn func(context.Context, *rest.Client) clprov.Provider +type newCacheStorageURIsClientFn func(*vim25.Client) clsutil.CacheStorageURIsClient +type newTopLevelDirectoryClientFn func(*vim25.Client) dsutil.CreateTopLevelDirectoryClient + +func newContentLibraryProviderOrDefault( + ctx context.Context) newContentLibraryProviderFn { + + out := clprov.NewProvider + obj := ctx.Value(internal.NewContentLibraryProviderContextKey) + if fn, ok := obj.(func(context.Context, *rest.Client) clprov.Provider); ok { + out = func(ctx context.Context, c *rest.Client) clprov.Provider { + if p := fn(ctx, c); p != nil { + return p + } + return clprov.NewProvider(ctx, c) + } + } + return out +} + +func newCacheStorageURIsClientOrDefault( + ctx context.Context) newCacheStorageURIsClientFn { + + out := newCacheStorageURIsClient + obj := ctx.Value(internal.NewCacheStorageURIsClientContextKey) + if fn, ok := obj.(func(*vim25.Client) clsutil.CacheStorageURIsClient); ok { + out = func(c *vim25.Client) clsutil.CacheStorageURIsClient { + if p := fn(c); p != nil { + return p + } + return newCacheStorageURIsClient(c) + } + } + return out +} + +func newTopLevelDirectoryClientOrDefault( + ctx context.Context) newTopLevelDirectoryClientFn { + + out := newTopLevelDirectoryClient + obj := ctx.Value(internal.NewTopLevelDirectoryClientContextKey) + if fn, ok := obj.(func(*vim25.Client) dsutil.CreateTopLevelDirectoryClient); ok { + out = func(c *vim25.Client) dsutil.CreateTopLevelDirectoryClient { + if p := fn(c); p != nil { + return p + } + return newTopLevelDirectoryClient(c) + } + } + return out +} + +func newTopLevelDirectoryClient(c *vim25.Client) dsutil.CreateTopLevelDirectoryClient { + return &createTopLevelDirectoryClient{ + FileManager: object.NewFileManager(c), + DatastoreNamespaceManager: object.NewDatastoreNamespaceManager(c), + } +} + +type createTopLevelDirectoryClient struct { + *object.FileManager + *object.DatastoreNamespaceManager +} + +func (c *createTopLevelDirectoryClient) WaitForTask( + ctx context.Context, task *object.Task) error { + + return task.Wait(ctx) +} + +func newCacheStorageURIsClient(c *vim25.Client) clsutil.CacheStorageURIsClient { + return &cacheStorageURIsClient{ + FileManager: object.NewFileManager(c), + VirtualDiskManager: object.NewVirtualDiskManager(c), + } +} + +type cacheStorageURIsClient struct { + *object.FileManager + *object.VirtualDiskManager +} + +func (c *cacheStorageURIsClient) WaitForTask( + ctx context.Context, task *object.Task) error { + + return task.Wait(ctx) +} diff --git a/controllers/virtualmachineimagecache/virtualmachineimagecache_controller_suite_test.go b/controllers/virtualmachineimagecache/virtualmachineimagecache_controller_suite_test.go new file mode 100644 index 000000000..25a2d742e --- /dev/null +++ b/controllers/virtualmachineimagecache/virtualmachineimagecache_controller_suite_test.go @@ -0,0 +1,24 @@ +// Copyright (c) 2024 Broadcom. All Rights Reserved. +// Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc. +// and/or its subsidiaries. + +package virtualmachineimagecache_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/vm-operator/controllers/virtualmachineimagecache" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" +) + +func TestVirtualMachineImageCacheController(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VirtualMachineImageCache Controller Test Suite") +} + +var _ = BeforeSuite(func() { + virtualmachineimagecache.SkipNameValidation = ptr.To(true) +}) diff --git a/controllers/virtualmachineimagecache/virtualmachineimagecache_controller_test.go b/controllers/virtualmachineimagecache/virtualmachineimagecache_controller_test.go new file mode 100644 index 000000000..eee77c16d --- /dev/null +++ b/controllers/virtualmachineimagecache/virtualmachineimagecache_controller_test.go @@ -0,0 +1,909 @@ +// // © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package virtualmachineimagecache_test + +import ( + "context" + "errors" + "fmt" + "path" + "time" + + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/ovf" + "github.com/vmware/govmomi/vapi/library" + "github.com/vmware/govmomi/vapi/rest" + "github.com/vmware/govmomi/vim25" + vimtypes "github.com/vmware/govmomi/vim25/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2/textlogger" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + ctrlmgr "sigs.k8s.io/controller-runtime/pkg/manager" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + "github.com/vmware-tanzu/vm-operator/controllers/virtualmachineimagecache" + "github.com/vmware-tanzu/vm-operator/controllers/virtualmachineimagecache/internal" + pkgcond "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + "github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels" + pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" + providerfake "github.com/vmware-tanzu/vm-operator/pkg/providers/fake" + clprov "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/contentlibrary" + vsclient "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/client" + dsutil "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/datastore" + clsutil "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/library" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +var _ = Describe( + "Reconcile", + Label( + testlabels.Controller, + testlabels.EnvTest, + testlabels.V1Alpha3, + ), + func() { + + const ( + fakeString = "fake" + + cndOVFReady = vmopv1.VirtualMachineImageCacheConditionOVFReady + cndDskReady = vmopv1.VirtualMachineImageCacheConditionDisksReady + cndPrvReady = vmopv1.VirtualMachineImageCacheConditionProviderReady + cndRdyReady = vmopv1.ReadyConditionType + ) + + getVMICacheObj := func( + namespace, + providerID, + providerVersion string, + locations ...vmopv1.VirtualMachineImageCacheLocationSpec) vmopv1.VirtualMachineImageCache { + + return vmopv1.VirtualMachineImageCache{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + GenerateName: "vmi-", + }, + Spec: vmopv1.VirtualMachineImageCacheSpec{ + ProviderID: providerID, + ProviderVersion: providerVersion, + Locations: locations, + }, + } + } + + var ( + ctx context.Context + vcSimCtx *builder.IntegrationTestContextForVCSim + provider *providerfake.VMProvider + initEnvFn builder.InitVCSimEnvFn + nsInfo builder.WorkloadNamespaceInfo + + itemID string + itemVersion string + + faker fakeClient + ) + + assertCondNil := func(g Gomega, o pkgcond.Getter, t string) { + g.ExpectWithOffset(1, pkgcond.Get(o, t)).To(BeNil()) + } + + assertCondTrue := func(g Gomega, o pkgcond.Getter, t string) { + c := pkgcond.Get(o, t) + g.ExpectWithOffset(1, c).ToNot(BeNil()) + g.ExpectWithOffset(1, c.Message).To(BeEmpty()) + g.ExpectWithOffset(1, c.Reason).To(Equal(string(metav1.ConditionTrue))) + g.ExpectWithOffset(1, c.Status).To(Equal(metav1.ConditionTrue)) + } + + assertCondFalse := func(g Gomega, o pkgcond.Getter, t, reason, message string) { + c := pkgcond.Get(o, t) + g.ExpectWithOffset(1, c).ToNot(BeNil()) + g.ExpectWithOffset(1, c.Message).To(HavePrefix(message)) + g.ExpectWithOffset(1, c.Reason).To(Equal(reason)) + g.ExpectWithOffset(1, c.Status).To(Equal(metav1.ConditionFalse)) + } + + assertOVFConfigMap := func(g Gomega, key ctrlclient.ObjectKey) { + var obj corev1.ConfigMap + g.ExpectWithOffset(1, vcSimCtx.Client.Get(ctx, key, &obj)).To(Succeed()) + g.ExpectWithOffset(1, obj.Data["value"]).To(MatchYAML(ovfEnvelopeYAML)) + } + + assertLocation := func( + g Gomega, + obj vmopv1.VirtualMachineImageCache, + locationIndex int) { + + var ( + spec = obj.Spec.Locations[locationIndex] + status = obj.Status.Locations[locationIndex] + ) + + g.ExpectWithOffset(1, status.DatacenterID).To(Equal(spec.DatacenterID)) + g.ExpectWithOffset(1, status.DatastoreID).To(Equal(spec.DatastoreID)) + + ds := object.NewDatastore( + vcSimCtx.VCClient.Client, + vimtypes.ManagedObjectReference{ + Type: "Datastore", + Value: spec.DatastoreID, + }) + dsName, err := ds.ObjectName(ctx) + g.ExpectWithOffset(1, err).ToNot(HaveOccurred()) + + topLevelCacheDir := fmt.Sprintf( + "[%s] %s", + dsName, + clsutil.TopLevelCacheDirName) + + itemCacheDir := clsutil.GetCacheDirForLibraryItem( + topLevelCacheDir, + obj.Spec.ProviderID, + obj.Spec.ProviderVersion) + + vmdkFileName := clsutil.GetCachedFileNameForVMDK( + "ttylinux-pc_i486-16.1-disk1.vmdk") + ".vmdk" + vmdkFilePath := path.Join(itemCacheDir, vmdkFileName) + + g.ExpectWithOffset(1, status.Files).To(HaveLen(1)) + g.ExpectWithOffset(1, status.Files[0]).To(Equal(vmdkFilePath)) + } + + Context("Ordered", Ordered, func() { + + BeforeAll(func() { + _ = internal.NewContentLibraryProviderContextKey + + ctx = context.Background() + ctx = logr.NewContext( + ctx, + textlogger.NewLogger(textlogger.NewConfig( + textlogger.Verbosity(5), + textlogger.Output(GinkgoWriter), + ))) + ctx = pkgcfg.WithContext(ctx, pkgcfg.Default()) + + ctx = context.WithValue( + ctx, + internal.NewContentLibraryProviderContextKey, + faker.newContentLibraryProviderFn) + ctx = context.WithValue( + ctx, + internal.NewCacheStorageURIsClientContextKey, + faker.newCacheStorageURIsClientFn) + ctx = context.WithValue( + ctx, + internal.NewTopLevelDirectoryClientContextKey, + faker.newCreateTopLevelDirectoryClientFn) + + provider = providerfake.NewVMProvider() + + vcSimCtx = builder.NewIntegrationTestContextForVCSim( + ctx, + builder.VCSimTestConfig{ + WithContentLibrary: true, + }, + virtualmachineimagecache.AddToManager, + func(ctx *pkgctx.ControllerManagerContext, _ ctrlmgr.Manager) error { + ctx.VMProvider = provider + return nil + }, + initEnvFn) + Expect(vcSimCtx).ToNot(BeNil()) + + vcSimCtx.BeforeEach() + ctx = vcSimCtx + + // Get the library item ID and content version. + itemID = vcSimCtx.ContentLibraryItemID + libMgr := library.NewManager(vcSimCtx.RestClient) + item, err := libMgr.GetLibraryItem(ctx, itemID) + Expect(err).ToNot(HaveOccurred()) + itemVersion = item.ContentVersion + + // Create one namespace for all the ordered tests. + nsInfo = vcSimCtx.CreateWorkloadNamespace() + }) + + BeforeEach(func() { + provider.VSphereClientFn = func(ctx context.Context) (*vsclient.Client, error) { + return vsclient.NewClient(ctx, vcSimCtx.VCClientConfig) + } + }) + + AfterEach(func() { + faker.reset() + }) + + AfterAll(func() { + vcSimCtx.AfterEach() + }) + + tableFn := func( + fn func() vmopv1.VirtualMachineImageCache, + expOVFRdy bool, expOVFMsg string, + expPrvRdy bool, expPrvMsg string, + expDskRdy bool, expDskMsg, expLocMsg string, + expRdyRdy bool, expRdyMsg string, + ) { + obj := fn() + Expect(vcSimCtx.Client.Create(ctx, &obj)).To(Succeed()) + key := ctrlclient.ObjectKey{ + Namespace: obj.Namespace, + Name: obj.Name, + } + + type expCondResult struct { + ready bool + msg string + } + + conditionTypesAndExpVals := map[string]expCondResult{ + cndOVFReady: { + ready: expOVFRdy, + msg: expOVFMsg, + }, + cndPrvReady: { + ready: expPrvRdy, + msg: expPrvMsg, + }, + cndDskReady: { + ready: expDskRdy, + msg: expDskMsg, + }, + cndRdyReady: { + ready: expRdyRdy, + msg: expRdyMsg, + }, + } + + Eventually(func(g Gomega) { + var obj vmopv1.VirtualMachineImageCache + g.Expect(vcSimCtx.Client.Get(ctx, key, &obj)).To(Succeed()) + + for t, v := range conditionTypesAndExpVals { + + switch { + case v.ready: + + // Verify the specified condition is true. + assertCondTrue(g, obj, t) + + switch t { + case cndOVFReady: + + // Verify the OVF is cached and ready. + assertOVFConfigMap(g, key) + + case cndDskReady: + + // Verify the disks are cached and ready. + g.Expect(obj.Status.Locations).To(HaveLen(1)) + assertCondTrue(g, obj.Status.Locations[0], cndRdyReady) + assertLocation(g, obj, 0) + + } + + case v.msg == "", + t == cndDskReady && len(obj.Spec.Locations) == 0: + + // There will be no condition when the expected + // message is empty OR spec.locations is empty and + // the tested condition is DisksReady. + assertCondNil(g, obj, t) + + case v.msg != "": + + // Expect a failure condition. + assertCondFalse(g, obj, t, "Failed", v.msg) + + if t == cndDskReady && expLocMsg != "" { + assertCondFalse( + g, + obj.Status.Locations[0], + cndRdyReady, + "Failed", + expLocMsg) + } + } + } + + }, 5*time.Second, 1*time.Second).Should(Succeed()) + } + + DescribeTable("Failures", + tableFn, + + Entry( + "spec.providerID is empty", + func() vmopv1.VirtualMachineImageCache { + return getVMICacheObj( + nsInfo.Namespace, + "", + fakeString) + }, + false, "", // OVFReady + false, "", // ProviderReady + false, "", "", // DisksReady + false, "spec.providerID is empty", // Ready + ), + + Entry( + "spec.providerVersion is empty", + func() vmopv1.VirtualMachineImageCache { + return getVMICacheObj( + nsInfo.Namespace, + fakeString, + "") + }, + false, "", // OVFReady + false, "", // ProviderReady + false, "", "", // DisksReady + false, "spec.providerVersion is empty", // Ready + ), + + Entry( + "failure to get vSphere client", + func() vmopv1.VirtualMachineImageCache { + provider.VSphereClientFn = func(ctx context.Context) (*vsclient.Client, error) { + return nil, errors.New("fubar") + } + return getVMICacheObj( + nsInfo.Namespace, + fakeString, + fakeString) + }, + false, "", // OVFReady + false, "", // ProviderReady + false, "", "", // DisksReady + false, "failed to get vSphere client: fubar", // Ready + ), + + Entry( + "library item does not exist", + func() vmopv1.VirtualMachineImageCache { + return getVMICacheObj( + nsInfo.Namespace, + fakeString, + fakeString) + }, + false, "failed to create or patch ovf configmap: failed to retrieve ovf envelope:", // OVFReady + false, "failed to get library item: failed to find image: "+fakeString, // ProviderReady + false, "", "", // DisksReady + false, "0 of 2 completed", // Ready + ), + + Entry( + "library item exists and location has invalid datacenter ID", + func() vmopv1.VirtualMachineImageCache { + return getVMICacheObj( + nsInfo.Namespace, + itemID, + itemVersion, + vmopv1.VirtualMachineImageCacheLocationSpec{ + DatacenterID: fakeString, + DatastoreID: vcSimCtx.Datastore.Reference().Value, + }) + }, + true, "", // OVFReady + true, "", // ProviderReady + false, "invalid datacenter ID", "", // DisksReady + false, "2 of 3 completed", // Ready + ), + + Entry( + "library item exists and location has invalid datastore ID", + func() vmopv1.VirtualMachineImageCache { + return getVMICacheObj( + nsInfo.Namespace, + itemID, + itemVersion, + vmopv1.VirtualMachineImageCacheLocationSpec{ + DatacenterID: vcSimCtx.Datacenter.Reference().Value, + DatastoreID: "invalid", + }) + }, + true, "", // OVFReady + true, "", // ProviderReady + false, "invalid datastore ID", "", // DisksReady + false, "2 of 3 completed", // Ready + ), + + Entry( + "cannot cache storage uris", + func() vmopv1.VirtualMachineImageCache { + + faker.fakeSRIClient = true + faker.queryVirtualDiskUuidFn = func( + context.Context, + string, + *object.Datacenter) (string, error) { + + return "", errors.New("query disk error") + } + + return getVMICacheObj( + nsInfo.Namespace, + itemID, + itemVersion, + vmopv1.VirtualMachineImageCacheLocationSpec{ + DatacenterID: vcSimCtx.Datacenter.Reference().Value, + DatastoreID: vcSimCtx.Datastore.Reference().Value, + }) + }, + true, "", // OVFReady + true, "", // ProviderReady + false, "0 of 1 completed", "failed to cache storage items: failed to query disk uuid: query disk error", // DisksReady + false, "2 of 3 completed", // Ready + ), + ) + + DescribeTable("Successes", + tableFn, + + Entry( + "no cache locations", + func() vmopv1.VirtualMachineImageCache { + return getVMICacheObj(nsInfo.Namespace, itemID, itemVersion) + }, + true, "", // OVFReady + true, "", // ProviderReady + false, "", "", // DisksReady + true, "", // Ready + ), + + Entry( + "a single cache location", + func() vmopv1.VirtualMachineImageCache { + return getVMICacheObj( + nsInfo.Namespace, + itemID, + itemVersion, + vmopv1.VirtualMachineImageCacheLocationSpec{ + DatacenterID: vcSimCtx.Datacenter.Reference().Value, + DatastoreID: vcSimCtx.Datastore.Reference().Value, + }) + }, + true, "", // OVFReady + true, "", // ProviderReady + true, "", "", // DisksReady + true, "", // Ready + ), + ) + }) + + }) + +type fakeClient struct { + fakeCLSProvdr bool + fakeSRIClient bool + fakeTLDClient bool + + queryVirtualDiskUuidFn func( //nolint:revive,stylecheck + ctx context.Context, + name string, + datacenter *object.Datacenter) (string, error) + + copyVirtualDiskFn func( + ctx context.Context, + srcName string, srcDatacenter *object.Datacenter, + dstName string, dstDatacenter *object.Datacenter, + dstSpec vimtypes.BaseVirtualDiskSpec, force bool) (*object.Task, error) + + makeDirectoryFn func( + ctx context.Context, + name string, + datacenter *object.Datacenter, + createParentDirectories bool) error + + waitForTaskFn func( + ctx context.Context, task *object.Task) error + + createDirectoryFn func( + ctx context.Context, + datastore *object.Datastore, + displayName, policy string) (string, error) + + convertNamespacePathToUuidPathFn func( //nolint:revive,stylecheck + ctx context.Context, + datacenter *object.Datacenter, + datastoreURL string) (string, error) + + getLibraryItemsFn func( + ctx context.Context, + libraryID string) ([]library.Item, error) + + getLibraryItemFn func( + ctx context.Context, + libraryID, + itemName string, + notFoundReturnErr bool) (*library.Item, error) + + getLibraryItemIDFn func( + ctx context.Context, + itemID string) (*library.Item, error) + + listLibraryItemsFn func( + ctx context.Context, + libraryID string) ([]string, error) + + updateLibraryItemFn func( + ctx context.Context, + itemID, + newName string, + newDescription *string) error + + retrieveOvfEnvelopeFromLibraryItemFn func( + ctx context.Context, + item *library.Item) (*ovf.Envelope, error) + + retrieveOvfEnvelopeByLibraryItemIDFn func( + ctx context.Context, + itemID string) (*ovf.Envelope, error) + + syncLibraryItemFn func( + ctx context.Context, + item *library.Item, + force bool) error + + listLibraryItemStorageFn func( + ctx context.Context, + itemID string) ([]library.Storage, error) + + resolveLibraryItemStorageFn func( + ctx context.Context, + datacenter *object.Datacenter, + storage []library.Storage) error + + createLibraryItemFn func( + ctx context.Context, + libraryItem library.Item, + path string) error +} + +func (m *fakeClient) reset() { + m.fakeCLSProvdr = false + m.fakeSRIClient = false + m.fakeTLDClient = false + + m.queryVirtualDiskUuidFn = nil + m.copyVirtualDiskFn = nil + m.makeDirectoryFn = nil + m.waitForTaskFn = nil + m.createDirectoryFn = nil + m.convertNamespacePathToUuidPathFn = nil + m.getLibraryItemsFn = nil + m.getLibraryItemFn = nil + m.getLibraryItemIDFn = nil + m.listLibraryItemsFn = nil + m.updateLibraryItemFn = nil + m.retrieveOvfEnvelopeFromLibraryItemFn = nil + m.retrieveOvfEnvelopeByLibraryItemIDFn = nil + m.syncLibraryItemFn = nil + m.listLibraryItemStorageFn = nil + m.resolveLibraryItemStorageFn = nil + m.createLibraryItemFn = nil +} + +func (m *fakeClient) newContentLibraryProviderFn( + context.Context, + *rest.Client) clprov.Provider { + + if m.fakeCLSProvdr { + return m + } + return nil +} + +func (m *fakeClient) newCacheStorageURIsClientFn( + c *vim25.Client) clsutil.CacheStorageURIsClient { + + if m.fakeSRIClient { + return m + } + return nil +} + +func (m *fakeClient) newCreateTopLevelDirectoryClientFn( + c *vim25.Client) dsutil.CreateTopLevelDirectoryClient { + + if m.fakeTLDClient { + return m + } + return nil +} + +func (m *fakeClient) QueryVirtualDiskUuid( //nolint:revive,stylecheck + ctx context.Context, + name string, + datacenter *object.Datacenter) (string, error) { + + if fn := m.queryVirtualDiskUuidFn; fn != nil { + return fn(ctx, name, datacenter) + } + return "", nil +} + +func (m *fakeClient) CopyVirtualDisk( + ctx context.Context, + srcName string, srcDatacenter *object.Datacenter, + dstName string, dstDatacenter *object.Datacenter, + dstSpec vimtypes.BaseVirtualDiskSpec, force bool) (*object.Task, error) { + + if fn := m.copyVirtualDiskFn; fn != nil { + return fn(ctx, srcName, srcDatacenter, dstName, dstDatacenter, dstSpec, force) + } + return nil, nil +} + +func (m *fakeClient) MakeDirectory( + ctx context.Context, + name string, + datacenter *object.Datacenter, + createParentDirectories bool) error { + + if fn := m.makeDirectoryFn; fn != nil { + return fn(ctx, name, datacenter, createParentDirectories) + } + return nil +} + +func (m *fakeClient) WaitForTask( + ctx context.Context, task *object.Task) error { + + if fn := m.waitForTaskFn; fn != nil { + return fn(ctx, task) + } + return nil +} + +func (m *fakeClient) CreateDirectory( + ctx context.Context, + datastore *object.Datastore, + displayName, policy string) (string, error) { + + if fn := m.createDirectoryFn; fn != nil { + return fn(ctx, datastore, displayName, policy) + } + return "", nil +} + +func (m *fakeClient) ConvertNamespacePathToUuidPath( //nolint:revive,stylecheck + ctx context.Context, + datacenter *object.Datacenter, + datastoreURL string) (string, error) { + + if fn := m.convertNamespacePathToUuidPathFn; fn != nil { + return fn(ctx, datacenter, datastoreURL) + } + return "", nil +} + +func (m *fakeClient) GetLibraryItems( + ctx context.Context, + libraryID string) ([]library.Item, error) { + + if fn := m.getLibraryItemsFn; fn != nil { + return fn(ctx, libraryID) + } + return nil, nil +} + +func (m *fakeClient) GetLibraryItem( + ctx context.Context, + libraryID, + itemName string, + notFoundReturnErr bool) (*library.Item, error) { + + if fn := m.getLibraryItemFn; fn != nil { + return fn(ctx, libraryID, itemName, notFoundReturnErr) + } + return nil, nil +} + +func (m *fakeClient) GetLibraryItemID( + ctx context.Context, + itemID string) (*library.Item, error) { + + if fn := m.getLibraryItemIDFn; fn != nil { + return fn(ctx, itemID) + } + return nil, nil +} + +func (m *fakeClient) ListLibraryItems( + ctx context.Context, + libraryID string) ([]string, error) { + + if fn := m.listLibraryItemsFn; fn != nil { + return fn(ctx, libraryID) + } + return nil, nil +} + +func (m *fakeClient) UpdateLibraryItem( + ctx context.Context, + itemID, + newName string, + newDescription *string) error { + + if fn := m.updateLibraryItemFn; fn != nil { + return fn(ctx, itemID, newName, newDescription) + } + return nil +} + +func (m *fakeClient) RetrieveOvfEnvelopeFromLibraryItem( + ctx context.Context, + item *library.Item) (*ovf.Envelope, error) { + + if fn := m.retrieveOvfEnvelopeFromLibraryItemFn; fn != nil { + return fn(ctx, item) + } + return nil, nil +} + +func (m *fakeClient) RetrieveOvfEnvelopeByLibraryItemID( + ctx context.Context, + itemID string) (*ovf.Envelope, error) { + + if fn := m.retrieveOvfEnvelopeByLibraryItemIDFn; fn != nil { + return fn(ctx, itemID) + } + return nil, nil +} + +func (m *fakeClient) SyncLibraryItem( + ctx context.Context, + item *library.Item, + force bool) error { + + if fn := m.syncLibraryItemFn; fn != nil { + return fn(ctx, item, force) + } + return nil +} + +func (m *fakeClient) ListLibraryItemStorage( + ctx context.Context, + itemID string) ([]library.Storage, error) { + + if fn := m.listLibraryItemStorageFn; fn != nil { + return fn(ctx, itemID) + } + return nil, nil +} + +func (m *fakeClient) ResolveLibraryItemStorage( + ctx context.Context, + datacenter *object.Datacenter, + storage []library.Storage) error { + + if fn := m.resolveLibraryItemStorageFn; fn != nil { + return fn(ctx, datacenter, storage) + } + return nil +} + +func (m *fakeClient) CreateLibraryItem( + ctx context.Context, + item library.Item, + path string) error { + + if fn := m.createLibraryItemFn; fn != nil { + return fn(ctx, item, path) + } + return nil +} + +const ovfEnvelopeYAML = ` +diskSection: + disk: + - capacity: "30" + capacityAllocationUnits: byte * 2^20 + diskId: vmdisk1 + fileRef: file1 + format: http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized + populatedSize: 18743296 + info: Virtual disk information +networkSection: + info: The list of logical networks + network: + - description: The nat network + name: nat +references: +- href: ttylinux-pc_i486-16.1-disk1.vmdk + id: file1 + size: 10595840 +virtualSystem: + id: vm + info: A virtual machine + name: ttylinux-pc_i486-16.1 + operatingSystemSection: + id: 36 + info: The kind of installed guest operating system + osType: otherLinuxGuest + virtualHardwareSection: + - config: + - key: firmware + required: false + value: efi + - key: powerOpInfo.powerOffType + required: false + value: soft + - key: powerOpInfo.resetType + required: false + value: soft + - key: powerOpInfo.suspendType + required: false + value: soft + - key: tools.syncTimeWithHost + required: false + value: "true" + - key: tools.toolsUpgradePolicy + required: false + value: upgradeAtPowerCycle + id: null + info: Virtual hardware requirements + item: + - allocationUnits: hertz * 10^6 + description: Number of Virtual CPUs + elementName: 1 virtual CPU(s) + instanceID: "1" + resourceType: 3 + virtualQuantity: 1 + - allocationUnits: byte * 2^20 + description: Memory Size + elementName: 32MB of memory + instanceID: "2" + resourceType: 4 + virtualQuantity: 32 + - address: "0" + description: IDE Controller + elementName: ideController0 + instanceID: "3" + resourceType: 5 + - addressOnParent: "0" + elementName: disk0 + hostResource: + - ovf:/disk/vmdisk1 + instanceID: "4" + parent: "3" + resourceType: 17 + - addressOnParent: "1" + automaticAllocation: true + config: + - key: wakeOnLanEnabled + required: false + value: "false" + connection: + - nat + description: E1000 ethernet adapter on "nat" + elementName: ethernet0 + instanceID: "5" + resourceSubType: E1000 + resourceType: 10 + - automaticAllocation: false + elementName: video + instanceID: "6" + required: false + resourceType: 24 + - automaticAllocation: false + elementName: vmci + instanceID: "7" + required: false + resourceSubType: vmware.vmci + resourceType: 1 + system: + elementName: Virtual Hardware Family + instanceID: "0" + virtualSystemIdentifier: ttylinux-pc_i486-16.1 + virtualSystemType: vmx-09` diff --git a/go.mod b/go.mod index ece694f04..e383ad68b 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/vmware-tanzu/vm-operator/external/tanzu-topology v0.0.0-00010101000000-000000000000 github.com/vmware-tanzu/vm-operator/pkg/backup/api v0.0.0-00010101000000-000000000000 github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels v0.0.0-00010101000000-000000000000 - github.com/vmware/govmomi v0.47.0-alpha.0.0.20241219162111-46d5d8739f1e + github.com/vmware/govmomi v0.47.0-alpha.0.0.20250102163115-80de29b4051e golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // * https://github.com/vmware-tanzu/vm-operator/security/dependabot/24 golang.org/x/text v0.21.0 diff --git a/go.sum b/go.sum index 3d98863af..516c375f0 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ github.com/vmware-tanzu/net-operator-api v0.0.0-20240523152550-862e2c4eb0e0 h1:y github.com/vmware-tanzu/net-operator-api v0.0.0-20240523152550-862e2c4eb0e0/go.mod h1:w6QJGm3crIA16ZIz1FVQXD2NVeJhOgGXxW05RbVTSTo= github.com/vmware-tanzu/nsx-operator/pkg/apis v0.0.0-20241112044858-9da8637c1b0d h1:z9lrzKVtNlujduv9BilzPxuge/LE2F0N1ms3TP4JZvw= github.com/vmware-tanzu/nsx-operator/pkg/apis v0.0.0-20241112044858-9da8637c1b0d/go.mod h1:Q4JzNkNMvjo7pXtlB5/R3oME4Nhah7fAObWgghVmtxk= -github.com/vmware/govmomi v0.47.0-alpha.0.0.20241219162111-46d5d8739f1e h1:xWk68LmieEMkyArGnvKLSn6APFUVbm/I5swqHX+3WGs= -github.com/vmware/govmomi v0.47.0-alpha.0.0.20241219162111-46d5d8739f1e/go.mod h1:bYwUHpGpisE4AOlDl5eph90T+cjJMIcKx/kaa5v5rQM= +github.com/vmware/govmomi v0.47.0-alpha.0.0.20250102163115-80de29b4051e h1:o49EvG0ToH1Oq6lg4fCOjFYoNi4K3K+5d9/yNmBstuY= +github.com/vmware/govmomi v0.47.0-alpha.0.0.20250102163115-80de29b4051e/go.mod h1:bYwUHpGpisE4AOlDl5eph90T+cjJMIcKx/kaa5v5rQM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/config/config.go b/pkg/config/config.go index 64bbb9019..84a66630c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -68,6 +68,11 @@ type Config struct { // Defaults to 10 seconds. PoweredOnVMHasIPRequeueDelay time.Duration + // SyncImageRequeueDelay is the requeue delay that is used to requeue an + // image that wants to be synced but is not yet ready. + // Defaults to 10 seconds. + SyncImageRequeueDelay time.Duration + NetworkProviderType NetworkProviderType VSphereNetworking bool LoadBalancerProvider string @@ -118,6 +123,21 @@ type Config struct { // // Defaults to false. AsyncCreateDisabled bool + + // FastDeployDefaultMode determines the default mode for Fast Deploy + // feature. + // + // Please note, this flag has no impact if the Fast Deploy feature is not + // enabled. + // + // The valid values are "direct" and "linked." If the FSS is enabled and: + // + // - the value is not valid, then "direct" mode is used. + // - the value is "direct," then the VM is deployed using cached disks. + // - the value is "linked," then the VM is deployed as a linked clone. + // + // Defaults to "direct." + FastDeployDefaultMode string } // GetMaxDeployThreadsOnProvider returns MaxDeployThreadsOnProvider if it is >0 diff --git a/pkg/config/default.go b/pkg/config/default.go index d09dedca3..98d791878 100644 --- a/pkg/config/default.go +++ b/pkg/config/default.go @@ -40,8 +40,10 @@ func Default() Config { MaxConcurrentReconciles: 1, AsyncSignalDisabled: false, AsyncCreateDisabled: false, + FastDeployDefaultMode: "direct", CreateVMRequeueDelay: 10 * time.Second, PoweredOnVMHasIPRequeueDelay: 10 * time.Second, + SyncImageRequeueDelay: 10 * time.Second, NetworkProviderType: NetworkProviderTypeNamed, PodName: defaultPrefix + "controller-manager", PodNamespace: defaultPrefix + "system", diff --git a/pkg/config/env.go b/pkg/config/env.go index a9539c481..751d6cb77 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -23,6 +23,7 @@ func FromEnv() Config { setInt(env.MaxCreateVMsOnProvider, &config.MaxCreateVMsOnProvider) setDuration(env.CreateVMRequeueDelay, &config.CreateVMRequeueDelay) setDuration(env.PoweredOnVMHasIPRequeueDelay, &config.PoweredOnVMHasIPRequeueDelay) + setDuration(env.SyncImageRequeueDelay, &config.SyncImageRequeueDelay) setNetworkProviderType(env.NetworkProviderType, &config.NetworkProviderType) setString(env.LoadBalancerProvider, &config.LoadBalancerProvider) setBool(env.VSphereNetworking, &config.VSphereNetworking) @@ -30,6 +31,7 @@ func FromEnv() Config { setBool(env.LogSensitiveData, &config.LogSensitiveData) setBool(env.AsyncSignalDisabled, &config.AsyncSignalDisabled) setBool(env.AsyncCreateDisabled, &config.AsyncCreateDisabled) + setString(env.FastDeployDefaultMode, &config.FastDeployDefaultMode) setDuration(env.InstanceStoragePVPlacementFailedTTL, &config.InstanceStorage.PVPlacementFailedTTL) setFloat64(env.InstanceStorageJitterMaxFactor, &config.InstanceStorage.JitterMaxFactor) diff --git a/pkg/config/env/env.go b/pkg/config/env/env.go index c99b61c28..46246f99e 100644 --- a/pkg/config/env/env.go +++ b/pkg/config/env/env.go @@ -18,6 +18,7 @@ const ( MaxCreateVMsOnProvider CreateVMRequeueDelay PoweredOnVMHasIPRequeueDelay + SyncImageRequeueDelay PrivilegedUsers NetworkProviderType LoadBalancerProvider @@ -27,6 +28,7 @@ const ( LogSensitiveData AsyncSignalDisabled AsyncCreateDisabled + FastDeployDefaultMode InstanceStoragePVPlacementFailedTTL InstanceStorageJitterMaxFactor InstanceStorageSeedRequeueDuration @@ -95,6 +97,8 @@ func (n VarName) String() string { return "CREATE_VM_REQUEUE_DELAY" case PoweredOnVMHasIPRequeueDelay: return "POWERED_ON_VM_HAS_IP_REQUEUE_DELAY" + case SyncImageRequeueDelay: + return "SYNC_IMAGE_REQUEUE_DELAY" case PrivilegedUsers: return "PRIVILEGED_USERS" case NetworkProviderType: @@ -113,6 +117,8 @@ func (n VarName) String() string { return "ASYNC_SIGNAL_DISABLED" case AsyncCreateDisabled: return "ASYNC_CREATE_DISABLED" + case FastDeployDefaultMode: + return "FAST_DEPLOY_DEFAULT_MODE" case InstanceStoragePVPlacementFailedTTL: return "INSTANCE_STORAGE_PV_PLACEMENT_FAILED_TTL" case InstanceStorageJitterMaxFactor: diff --git a/pkg/config/env_test.go b/pkg/config/env_test.go index cd64740fa..11cbaa268 100644 --- a/pkg/config/env_test.go +++ b/pkg/config/env_test.go @@ -79,6 +79,7 @@ var _ = Describe( Expect(os.Setenv("MAX_CONCURRENT_RECONCILES", "114")).To(Succeed()) Expect(os.Setenv("ASYNC_SIGNAL_DISABLED", "true")).To(Succeed()) Expect(os.Setenv("ASYNC_CREATE_DISABLED", "true")).To(Succeed()) + Expect(os.Setenv("FAST_DEPLOY_DEFAULT_MODE", "linked")).To(Succeed()) Expect(os.Setenv("LEADER_ELECTION_ID", "115")).To(Succeed()) Expect(os.Setenv("POD_NAME", "116")).To(Succeed()) Expect(os.Setenv("POD_NAMESPACE", "117")).To(Succeed()) @@ -105,6 +106,7 @@ var _ = Describe( Expect(os.Setenv("FSS_WCP_VMSERVICE_FAST_DEPLOY", "true")).To(Succeed()) Expect(os.Setenv("CREATE_VM_REQUEUE_DELAY", "125h")).To(Succeed()) Expect(os.Setenv("POWERED_ON_VM_HAS_IP_REQUEUE_DELAY", "126h")).To(Succeed()) + Expect(os.Setenv("SYNC_IMAGE_REQUEUE_DELAY", "127h")).To(Succeed()) }) It("Should return a default config overridden by the environment", func() { Expect(config).To(BeComparableTo(pkgcfg.Config{ @@ -129,6 +131,7 @@ var _ = Describe( MaxConcurrentReconciles: 114, AsyncSignalDisabled: true, AsyncCreateDisabled: true, + FastDeployDefaultMode: "linked", LeaderElectionID: "115", PodName: "116", PodNamespace: "117", @@ -156,6 +159,7 @@ var _ = Describe( }, CreateVMRequeueDelay: 125 * time.Hour, PoweredOnVMHasIPRequeueDelay: 126 * time.Hour, + SyncImageRequeueDelay: 127 * time.Hour, })) }) }) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 193e6cfaf..18303e925 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -38,4 +38,24 @@ const ( // MinSupportedHWVersionForPCIPassthruDevices is the supported virtual // hardware version for NVidia PCI devices. MinSupportedHWVersionForPCIPassthruDevices = vimtypes.VMX17 + + // VMICacheLabelKey is applied to resources that need to be reconciled when + // the VirtualMachineImageCache resource specified by the label's value is + // updated. + VMICacheLabelKey = "vmoperator.vmware.com/vmi-cache" + + // FastDeployAnnotationKey is applied to VirtualMachine resources that want + // to control the mode of FastDeploy used to create the underlying VM. + // Please note, this annotation only has any effect if the FastDeploy FSS is + // enabled. + // The valid values for this annotation are "direct," "linked," and + // "disabled," and "false." If the FSS is enabled and: + // + // - the value is "direct," then the VM is deployed from cached disks. + // - the value is "linked," then the VM is deployed as a linked clone. + // - the value is "disabled" or "false", then fast deploy is not used to + // deploy the VM. + // - the value is invalid or the annotation is not present, then the mode + // is derived from the environment variable FAST_DEPLOY_DEFAULT_MODE. + FastDeployAnnotationKey = "vmoperator.vmware.com/fast-deploy" ) diff --git a/pkg/errors/requeue_error.go b/pkg/errors/requeue_error.go new file mode 100644 index 000000000..f2155b971 --- /dev/null +++ b/pkg/errors/requeue_error.go @@ -0,0 +1,42 @@ +// // © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package errors + +import ( + "errors" + "fmt" + "time" + + ctrl "sigs.k8s.io/controller-runtime" +) + +// RequeueError may be returned from any part of a reconcile call stack and the +// controller should requeue the request. If After > 0 then the request is +// requeued with the provided value, otherwise the request is requeued +// immediately. +type RequeueError struct { + After time.Duration +} + +func (e RequeueError) Error() string { + if e.After == 0 { + return "requeue immediately" + } + return fmt.Sprintf("requeue after %s", e.After) +} + +// ResultFromError returns a ReconcileResult based on the provided error. If +// the error contains an embedded RequeueError, then it is used to influence +// the result. +func ResultFromError(err error) (ctrl.Result, error) { + var dst *RequeueError + if err != nil && errors.As(err, &dst) { + if dst.After == 0 { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{RequeueAfter: dst.After}, nil + } + return ctrl.Result{}, err +} diff --git a/pkg/providers/vsphere/contentlibrary/content_library_provider.go b/pkg/providers/vsphere/contentlibrary/content_library_provider.go index 8f74f43c9..2dd88a777 100644 --- a/pkg/providers/vsphere/contentlibrary/content_library_provider.go +++ b/pkg/providers/vsphere/contentlibrary/content_library_provider.go @@ -17,8 +17,10 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "github.com/go-logr/logr" + "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/ovf" "github.com/vmware/govmomi/vapi/library" + "github.com/vmware/govmomi/vapi/library/finder" "github.com/vmware/govmomi/vapi/rest" "github.com/vmware/govmomi/vim25/soap" @@ -35,6 +37,9 @@ type Provider interface { UpdateLibraryItem(ctx context.Context, itemID, newName string, newDescription *string) error RetrieveOvfEnvelopeFromLibraryItem(ctx context.Context, item *library.Item) (*ovf.Envelope, error) RetrieveOvfEnvelopeByLibraryItemID(ctx context.Context, itemID string) (*ovf.Envelope, error) + SyncLibraryItem(ctx context.Context, item *library.Item, force bool) error + ListLibraryItemStorage(ctx context.Context, itemID string) ([]library.Storage, error) + ResolveLibraryItemStorage(ctx context.Context, datacenter *object.Datacenter, storage []library.Storage) error // TODO: Testing only. Remove these from this file. CreateLibraryItem(ctx context.Context, libraryItem library.Item, path string) error @@ -245,6 +250,16 @@ func (cs *provider) UpdateLibraryItem(ctx context.Context, itemID, newName strin return cs.libMgr.UpdateLibraryItem(ctx, item) } +// SyncLibraryItem issues a sync call against a subscribed library item, +// fetching its latest OVF and disks. +func (cs *provider) SyncLibraryItem( + ctx context.Context, + item *library.Item, + force bool) error { + + return cs.libMgr.SyncLibraryItem(ctx, item, force) +} + // Only used in testing. func (cs *provider) CreateLibraryItem(ctx context.Context, libraryItem library.Item, path string) error { log.Info("Creating Library Item", "item", libraryItem, "path", path) @@ -380,3 +395,29 @@ func (cs *provider) generateDownloadURLForLibraryItem( return url.Parse(fileURL) } + +func (cs *provider) ListLibraryItemStorage( + ctx context.Context, + itemID string) ([]library.Storage, error) { + + return cs.libMgr.ListLibraryItemStorage(ctx, itemID) +} + +func (cs *provider) ResolveLibraryItemStorage( + ctx context.Context, + datacenter *object.Datacenter, + storage []library.Storage) error { + + if err := finder.NewPathFinder( + cs.libMgr, + datacenter.Client()).ResolveLibraryItemStorage( + ctx, + datacenter, + nil, + storage); err != nil { + + return fmt.Errorf("failed to resolve library item storage URIs: %w", err) + } + + return nil +} diff --git a/pkg/providers/vsphere/contentlibrary/content_library_test.go b/pkg/providers/vsphere/contentlibrary/content_library_test.go index b8fcf311d..b03e1a6d4 100644 --- a/pkg/providers/vsphere/contentlibrary/content_library_test.go +++ b/pkg/providers/vsphere/contentlibrary/content_library_test.go @@ -119,13 +119,13 @@ func clTests() { libItem := library.Item{ Name: libItemName, Type: "ovf", - LibraryID: ctx.ContentLibraryID, + LibraryID: ctx.LocalContentLibraryID, } err = clProvider.CreateLibraryItem(ctx, libItem, ovfPath) Expect(err).NotTo(HaveOccurred()) - libItem2, err := clProvider.GetLibraryItem(ctx, ctx.ContentLibraryID, libItemName, true) + libItem2, err := clProvider.GetLibraryItem(ctx, ctx.LocalContentLibraryID, libItemName, true) Expect(err).ToNot(HaveOccurred()) Expect(libItem2).ToNot(BeNil()) Expect(libItem2.Name).To(Equal(libItem.Name)) diff --git a/pkg/providers/vsphere/placement/zone_placement.go b/pkg/providers/vsphere/placement/zone_placement.go index 791db4680..d97eea139 100644 --- a/pkg/providers/vsphere/placement/zone_placement.go +++ b/pkg/providers/vsphere/placement/zone_placement.go @@ -57,6 +57,7 @@ type DatastoreResult struct { MoRef vimtypes.ManagedObjectReference URL string TopLevelDirectoryCreateSupported bool + SectorFormat string // ForDisk is false if the recommendation is for the VM's home directory and // true if for a disk. DiskKey is only valid if ForDisk is true. @@ -493,6 +494,8 @@ func getDatastoreNameAndType( Type: "Datastore", PathSet: []string{ "capability.topLevelDirectoryCreateSupported", + // TODO(akutz) Support 4kn + // "info.supportedVDiskFormats", "info.url", "name", }, diff --git a/pkg/providers/vsphere/virtualmachine/publish_test.go b/pkg/providers/vsphere/virtualmachine/publish_test.go index 25a1372bd..11910a43c 100644 --- a/pkg/providers/vsphere/virtualmachine/publish_test.go +++ b/pkg/providers/vsphere/virtualmachine/publish_test.go @@ -40,7 +40,7 @@ func publishTests() { vm = builder.DummyVirtualMachine() vm.Status.UniqueID = vcVM.Reference().Value - cl = builder.DummyContentLibrary("dummy-cl", "dummy-ns", ctx.ContentLibraryID) + cl = builder.DummyContentLibrary("dummy-cl", "dummy-ns", ctx.LocalContentLibraryID) vmPub = builder.DummyVirtualMachinePublishRequest("dummy-vmpub", "dummy-ns", vcVM.Name(), "dummy-item-name", "dummy-cl") vmPub.Status.SourceRef = &vmPub.Spec.Source diff --git a/pkg/providers/vsphere/vmlifecycle/create.go b/pkg/providers/vsphere/vmlifecycle/create.go index 0428e018e..d823d08e2 100644 --- a/pkg/providers/vsphere/vmlifecycle/create.go +++ b/pkg/providers/vsphere/vmlifecycle/create.go @@ -6,7 +6,6 @@ package vmlifecycle import ( "github.com/vmware/govmomi/find" - "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/vapi/rest" "github.com/vmware/govmomi/vim25" vimtypes "github.com/vmware/govmomi/vim25/types" @@ -22,12 +21,14 @@ type CreateArgs struct { ConfigSpec vimtypes.VirtualMachineConfigSpec StorageProvisioning string + DatacenterMoID string FolderMoID string ResourcePoolMoID string HostMoID string StorageProfileID string DatastoreMoID string // gce2e only: used only if StorageProfileID is unset Datastores []DatastoreRef + DiskPaths []string ZoneName string } @@ -49,18 +50,10 @@ func CreateVirtualMachine( restClient *rest.Client, vimClient *vim25.Client, finder *find.Finder, - datacenter *object.Datacenter, createArgs *CreateArgs) (*vimtypes.ManagedObjectReference, error) { if createArgs.UseContentLibrary { - return deployFromContentLibrary( - vmCtx, - k8sClient, - restClient, - vimClient, - datacenter, - createArgs) + return deployFromContentLibrary(vmCtx, restClient, vimClient, createArgs) } - return cloneVMFromInventory(vmCtx, finder, createArgs) } diff --git a/pkg/providers/vsphere/vmlifecycle/create_contentlibrary.go b/pkg/providers/vsphere/vmlifecycle/create_contentlibrary.go index 8c6df7555..bf46c18dc 100644 --- a/pkg/providers/vsphere/vmlifecycle/create_contentlibrary.go +++ b/pkg/providers/vsphere/vmlifecycle/create_contentlibrary.go @@ -14,7 +14,6 @@ import ( "github.com/vmware/govmomi/vapi/vcenter" "github.com/vmware/govmomi/vim25" vimtypes "github.com/vmware/govmomi/vim25/types" - ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" @@ -117,10 +116,8 @@ func deployVMTX( func deployFromContentLibrary( vmCtx pkgctx.VirtualMachineContext, - k8sClient ctrlclient.Client, restClient *rest.Client, vimClient *vim25.Client, - datacenter *object.Datacenter, createArgs *CreateArgs) (*vimtypes.ManagedObjectReference, error) { // This call is needed to get the item type. We could avoid going to CL here, and @@ -134,14 +131,7 @@ func deployFromContentLibrary( switch item.Type { case library.ItemTypeOVF: if pkgcfg.FromContext(vmCtx).Features.FastDeploy { - return linkedCloneOVF( - vmCtx, - k8sClient, - vimClient, - restClient, - datacenter, - item, - createArgs) + return fastDeploy(vmCtx, vimClient, createArgs) } return deployOVF(vmCtx, restClient, item, createArgs) case library.ItemTypeVMTX: diff --git a/pkg/providers/vsphere/vmlifecycle/create_contentlibrary_linked_clone.go b/pkg/providers/vsphere/vmlifecycle/create_contentlibrary_linked_clone.go deleted file mode 100644 index 085258eb9..000000000 --- a/pkg/providers/vsphere/vmlifecycle/create_contentlibrary_linked_clone.go +++ /dev/null @@ -1,226 +0,0 @@ -// © Broadcom. All Rights Reserved. -// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. -// SPDX-License-Identifier: Apache-2.0 - -package vmlifecycle - -import ( - "context" - "errors" - "fmt" - "path" - - "github.com/vmware/govmomi/object" - "github.com/vmware/govmomi/vapi/library" - "github.com/vmware/govmomi/vapi/rest" - "github.com/vmware/govmomi/vim25" - vimtypes "github.com/vmware/govmomi/vim25/types" - ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - - pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" - "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" - vmopv1util "github.com/vmware-tanzu/vm-operator/pkg/util/vmopv1" - clsutil "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/library" -) - -func linkedCloneOVF( - vmCtx pkgctx.VirtualMachineContext, - k8sClient ctrlclient.Client, - vimClient *vim25.Client, - restClient *rest.Client, - datacenter *object.Datacenter, - item *library.Item, - createArgs *CreateArgs) (*vimtypes.ManagedObjectReference, error) { - - logger := vmCtx.Logger.WithName("linkedCloneOVF") - - if len(createArgs.Datastores) == 0 { - return nil, errors.New("no compatible datastores") - } - - // Get the information required to do the linked clone. - imgInfo, err := getImageLinkedCloneInfo( - vmCtx, - k8sClient, - restClient, - item) - if err != nil { - return nil, err - } - - topLevelCacheDir, err := clsutil.GetTopLevelCacheDir( - vmCtx, - object.NewDatastoreNamespaceManager(vimClient), - datacenter, - object.NewDatastore(vimClient, createArgs.Datastores[0].MoRef), - createArgs.Datastores[0].Name, - createArgs.Datastores[0].URL, - createArgs.Datastores[0].TopLevelDirectoryCreateSupported) - if err != nil { - return nil, fmt.Errorf("failed to create top-level cache dir: %w", err) - } - logger.Info("Got top-level cache dir", "topLevelCacheDir", topLevelCacheDir) - - dstDir := clsutil.GetCacheDirForLibraryItem( - topLevelCacheDir, - imgInfo.ItemID, - imgInfo.ItemContentVersion) - logger.Info("Got item cache dir", "dstDir", dstDir) - - dstURIs, err := clsutil.CacheStorageURIs( - vmCtx, - newCacheStorageURIsClient(vimClient), - datacenter, - datacenter, - dstDir, - imgInfo.DiskURIs...) - if err != nil { - return nil, fmt.Errorf("failed to cache library item disks: %w", err) - } - logger.Info("Got parent disks", "dstURIs", dstURIs) - - vmDir := path.Dir(createArgs.ConfigSpec.Files.VmPathName) - logger.Info("Got vm dir", "vmDir", vmDir) - - // Update the ConfigSpec with the disk chains. - var disks []*vimtypes.VirtualDisk - for i := range createArgs.ConfigSpec.DeviceChange { - dc := createArgs.ConfigSpec.DeviceChange[i].GetVirtualDeviceConfigSpec() - if d, ok := dc.Device.(*vimtypes.VirtualDisk); ok { - disks = append(disks, d) - - // The profile is no longer needed since we have placement. - dc.Profile = nil - } - } - logger.Info("Got disks", "disks", disks) - - if a, b := len(dstURIs), len(disks); a != b { - return nil, fmt.Errorf( - "invalid disk count: len(uris)=%d, len(disks)=%d", a, b) - } - - for i := range disks { - d := disks[i] - if bfb, ok := d.Backing.(vimtypes.BaseVirtualDeviceFileBackingInfo); ok { - fb := bfb.GetVirtualDeviceFileBackingInfo() - fb.Datastore = &createArgs.Datastores[0].MoRef - fb.FileName = fmt.Sprintf("%s/%s-%d.vmdk", vmDir, vmCtx.VM.Name, i) - } - if fb, ok := d.Backing.(*vimtypes.VirtualDiskFlatVer2BackingInfo); ok { - fb.Parent = &vimtypes.VirtualDiskFlatVer2BackingInfo{ - VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ - Datastore: &createArgs.Datastores[0].MoRef, - FileName: dstURIs[i], - }, - DiskMode: string(vimtypes.VirtualDiskModePersistent), - ThinProvisioned: ptr.To(true), - } - } - } - - // The profile is no longer needed since we have placement. - createArgs.ConfigSpec.VmProfile = nil - - vmCtx.Logger.Info( - "Deploying OVF Library Item as linked clone", - "itemID", item.ID, - "itemName", item.Name, - "configSpec", createArgs.ConfigSpec) - - folder := object.NewFolder( - vimClient, - vimtypes.ManagedObjectReference{ - Type: "Folder", - Value: createArgs.FolderMoID, - }) - pool := object.NewResourcePool( - vimClient, - vimtypes.ManagedObjectReference{ - Type: "ResourcePool", - Value: createArgs.ResourcePoolMoID, - }) - - createTask, err := folder.CreateVM( - vmCtx, - createArgs.ConfigSpec, - pool, - nil) - if err != nil { - return nil, fmt.Errorf("failed to call create task: %w", err) - } - - createTaskInfo, err := createTask.WaitForResult(vmCtx) - if err != nil { - return nil, fmt.Errorf("failed to wait for create task: %w", err) - } - - vmRef, ok := createTaskInfo.Result.(vimtypes.ManagedObjectReference) - if !ok { - return nil, fmt.Errorf( - "failed to assert create task result is ref: %[1]T %+[1]v", - createTaskInfo.Result) - } - - return &vmRef, nil -} - -func getImageLinkedCloneInfo( - vmCtx pkgctx.VirtualMachineContext, - k8sClient ctrlclient.Client, - restClient *rest.Client, - item *library.Item) (vmopv1util.ImageDiskInfo, error) { - - imgInfo, err := vmopv1util.GetImageDiskInfo( - vmCtx, - k8sClient, - *vmCtx.VM.Spec.Image, - vmCtx.VM.Namespace) - - if err == nil { - return imgInfo, nil - } - - if !errors.Is(err, vmopv1util.ErrImageNotSynced) { - return vmopv1util.ImageDiskInfo{}, - fmt.Errorf("failed to get image info before syncing: %w", err) - } - - if err := clsutil.SyncLibraryItem( - vmCtx, - library.NewManager(restClient), - item.ID); err != nil { - - return vmopv1util.ImageDiskInfo{}, fmt.Errorf("failed to sync: %w", err) - } - - if imgInfo, err = vmopv1util.GetImageDiskInfo( - vmCtx, - k8sClient, - *vmCtx.VM.Spec.Image, - vmCtx.VM.Namespace); err != nil { - - return vmopv1util.ImageDiskInfo{}, - fmt.Errorf("failed to get image info after syncing: %w", err) - } - - return imgInfo, nil -} - -func newCacheStorageURIsClient(c *vim25.Client) clsutil.CacheStorageURIsClient { - return &cacheStorageURIsClient{ - FileManager: object.NewFileManager(c), - VirtualDiskManager: object.NewVirtualDiskManager(c), - } -} - -type cacheStorageURIsClient struct { - *object.FileManager - *object.VirtualDiskManager -} - -func (c *cacheStorageURIsClient) WaitForTask( - ctx context.Context, task *object.Task) error { - - return task.Wait(ctx) -} diff --git a/pkg/providers/vsphere/vmlifecycle/create_fastdeploy.go b/pkg/providers/vsphere/vmlifecycle/create_fastdeploy.go new file mode 100644 index 000000000..c374968e5 --- /dev/null +++ b/pkg/providers/vsphere/vmlifecycle/create_fastdeploy.go @@ -0,0 +1,315 @@ +// © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vmlifecycle + +import ( + "context" + "errors" + "fmt" + "path" + "strings" + + "github.com/go-logr/logr" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25" + vimtypes "github.com/vmware/govmomi/vim25/types" + + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" + pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" +) + +func fastDeploy( + vmCtx pkgctx.VirtualMachineContext, + vimClient *vim25.Client, + createArgs *CreateArgs) (vmRef *vimtypes.ManagedObjectReference, retErr error) { + + logger := vmCtx.Logger.WithName("fastDeploy") + + if len(createArgs.Datastores) == 0 { + return nil, errors.New("no compatible datastores") + } + + vmDir := path.Dir(createArgs.ConfigSpec.Files.VmPathName) + logger.Info("Got vm dir", "vmDir", vmDir) + + srcDiskPaths := createArgs.DiskPaths + logger.Info("Got source disk paths", "srcDiskPaths", srcDiskPaths) + + dstDiskPaths := make([]string, len(srcDiskPaths)) + for i := 0; i < len(dstDiskPaths); i++ { + dstDiskPaths[i] = fmt.Sprintf("%s/disk-%d.vmdk", vmDir, i) + } + logger.Info("Got destination disk paths", "dstDiskPaths", dstDiskPaths) + + // Remove the storage profile from the VM. + createArgs.ConfigSpec.VmProfile = nil + + // Collect the disks and remove the storage profile from them. + var ( + disks []*vimtypes.VirtualDisk + diskSpecs []*vimtypes.VirtualDeviceConfigSpec + ) + for i := range createArgs.ConfigSpec.DeviceChange { + dc := createArgs.ConfigSpec.DeviceChange[i].GetVirtualDeviceConfigSpec() + if d, ok := dc.Device.(*vimtypes.VirtualDisk); ok { + disks = append(disks, d) + diskSpecs = append(diskSpecs, dc) + } + } + + if a, b := len(srcDiskPaths), len(disks); a != b { + logger.Info("Got disks", "disks", disks) + return nil, fmt.Errorf( + "invalid disk count: len(srcDiskPaths)=%d, len(disks)=%d", a, b) + } + + // Update the disks with their expected file names. + for i := range disks { + d := disks[i] + if bfb, ok := d.Backing.(vimtypes.BaseVirtualDeviceFileBackingInfo); ok { + fb := bfb.GetVirtualDeviceFileBackingInfo() + fb.Datastore = &createArgs.Datastores[0].MoRef + fb.FileName = dstDiskPaths[i] + } + } + logger.Info("Got disks", "disks", disks) + + datacenter := object.NewDatacenter( + vimClient, + vimtypes.ManagedObjectReference{ + Type: string(vimtypes.ManagedObjectTypesDatacenter), + Value: createArgs.DatacenterMoID, + }) + logger.Info("Got datacenter", "datacenter", datacenter.Reference()) + + // Create the directory where the VM will be created. + fm := object.NewFileManager(vimClient) + if err := fm.MakeDirectory(vmCtx, vmDir, datacenter, true); err != nil { + return nil, fmt.Errorf("failed to create vm dir %q: %w", vmDir, err) + } + + // If any error occurs after this point, the newly created VM directory and + // its contents need to be cleaned up. + defer func() { + if retErr == nil { + // Do not delete the VM directory if this function was successful. + return + } + + // Use a new context to ensure cleanup happens even if the context + // is cancelled. + ctx := context.Background() + + // Delete the VM directory and its contents. + t, err := fm.DeleteDatastoreFile(ctx, vmDir, datacenter) + if err != nil { + err = fmt.Errorf( + "failed to call delete api for vm dir %q: %w", vmDir, err) + if retErr == nil { + retErr = err + } else { + retErr = fmt.Errorf("%w,%w", err, retErr) + } + return + } + + // Wait for the delete call to return. + if err := t.Wait(ctx); err != nil { + err = fmt.Errorf("failed to delete vm dir %q: %w", vmDir, err) + if retErr == nil { + retErr = err + } else { + retErr = fmt.Errorf("%w,%w", err, retErr) + } + } + }() + + folder := object.NewFolder(vimClient, vimtypes.ManagedObjectReference{ + Type: "Folder", + Value: createArgs.FolderMoID, + }) + logger.Info("Got folder", "folder", folder.Reference()) + + pool := object.NewResourcePool(vimClient, vimtypes.ManagedObjectReference{ + Type: "ResourcePool", + Value: createArgs.ResourcePoolMoID, + }) + logger.Info("Got pool", "pool", pool.Reference()) + + // Determine the type of fast deploy operation. + fastDeployMode := vmCtx.VM.Annotations[pkgconst.FastDeployAnnotationKey] + if fastDeployMode == "" { + fastDeployMode = pkgcfg.FromContext(vmCtx).FastDeployDefaultMode + } + logger.Info( + "Deploying OVF Library Item with Fast Deploy", + "mode", fastDeployMode) + + if strings.EqualFold(fastDeployMode, "linked") { + fastDeployLinked( + createArgs.Datastores[0].MoRef, + disks, + diskSpecs, + srcDiskPaths) + } else if err := fastDeployDirect( + vmCtx, + datacenter, + createArgs.ConfigSpec.Crypto, + disks, + diskSpecs, + dstDiskPaths, + srcDiskPaths); err != nil { + + return nil, err + } + + logger.Info("Creating VM", + "configSpec", vimtypes.ToString(createArgs.ConfigSpec)) + + createTask, err := folder.CreateVM( + vmCtx, + createArgs.ConfigSpec, + pool, + nil) + if err != nil { + return nil, fmt.Errorf("failed to call create task: %w", err) + } + + createTaskInfo, err := createTask.WaitForResult(vmCtx) + if err != nil { + return nil, fmt.Errorf("failed to wait for create task: %w", err) + } + + vmRefVal, ok := createTaskInfo.Result.(vimtypes.ManagedObjectReference) + if !ok { + return nil, fmt.Errorf( + "failed to assert create task result is ref: %[1]T %+[1]v", + createTaskInfo.Result) + } + + return &vmRefVal, nil +} + +func fastDeployLinked( + datastoreRef vimtypes.ManagedObjectReference, + disks []*vimtypes.VirtualDisk, + diskSpecs []*vimtypes.VirtualDeviceConfigSpec, + srcDiskPaths []string) { + + for i := range diskSpecs { + // The profile is no longer needed since we have placement and linked + // clones do not support disk encryption. + diskSpecs[i].Profile = nil + } + + for i := range disks { + fileBackingInfo := vimtypes.VirtualDeviceFileBackingInfo{ + Datastore: &datastoreRef, + FileName: srcDiskPaths[i], + } + switch tBack := disks[i].Backing.(type) { + case *vimtypes.VirtualDiskFlatVer2BackingInfo: + // Linked clones do not support disk encryption. + tBack.KeyId = nil + + // Point the disk to its parent. + tBack.Parent = &vimtypes.VirtualDiskFlatVer2BackingInfo{ + VirtualDeviceFileBackingInfo: fileBackingInfo, + DiskMode: string(vimtypes.VirtualDiskModePersistent), + ThinProvisioned: ptr.To(true), + } + case *vimtypes.VirtualDiskSeSparseBackingInfo: + // Linked clones do not support disk encryption. + tBack.KeyId = nil + + // Point the disk to its parent. + tBack.Parent = &vimtypes.VirtualDiskSeSparseBackingInfo{ + VirtualDeviceFileBackingInfo: fileBackingInfo, + DiskMode: string(vimtypes.VirtualDiskModePersistent), + } + case *vimtypes.VirtualDiskSparseVer2BackingInfo: + // Linked clones do not support disk encryption. + tBack.KeyId = nil + + // Point the disk to its parent. + tBack.Parent = &vimtypes.VirtualDiskSparseVer2BackingInfo{ + VirtualDeviceFileBackingInfo: fileBackingInfo, + DiskMode: string(vimtypes.VirtualDiskModePersistent), + } + } + } +} + +func fastDeployDirect( + ctx context.Context, + datacenter *object.Datacenter, + cryptoSpec vimtypes.BaseCryptoSpec, + disks []*vimtypes.VirtualDisk, + diskSpecs []*vimtypes.VirtualDeviceConfigSpec, + dstDiskPaths, + srcDiskPaths []string) error { + + logger := logr.FromContextOrDiscard(ctx).WithName("fastDeployDirect") + + // Copy each disk into the VM directory. + var ( + copyDiskTasks = make([]*object.Task, len(srcDiskPaths)) + copyDiskSpec = &vimtypes.FileBackedVirtualDiskSpec{ + VirtualDiskSpec: vimtypes.VirtualDiskSpec{ + AdapterType: string(vimtypes.VirtualDiskAdapterTypeLsiLogic), + DiskType: string(vimtypes.VirtualDiskTypeThin), + }, + // The disk is encrypted (or not) using the same settings as the VM. + Crypto: cryptoSpec, + } + diskManager = object.NewVirtualDiskManager(datacenter.Client()) + ) + for i := range srcDiskPaths { + s := srcDiskPaths[i] + d := dstDiskPaths[i] + + logger.Info("Copying disk", "dst", d, "src", s) + t, err := diskManager.CopyVirtualDisk( + ctx, + s, + datacenter, + d, + datacenter, + copyDiskSpec, + false) + if err != nil { + return fmt.Errorf("failed to copy disk %q to %q: %w", s, d, err) + } + copyDiskTasks[i] = t + } + + for i := range diskSpecs { + // The operation for each disk is empty because the disk already exists. + diskSpecs[i].FileOperation = "" + } + + // If the disks were encrypted on copy, then update the disks in the VM's + // ConfigSpec to specify the encryption key. + var keyID *vimtypes.CryptoKeyId + if v, ok := copyDiskSpec.Crypto.(*vimtypes.CryptoSpecEncrypt); ok { + keyID = &v.CryptoKeyId + } + if keyID != nil { + for i := range disks { + switch tBack := disks[i].Backing.(type) { + case *vimtypes.VirtualDiskFlatVer2BackingInfo: + tBack.KeyId = keyID + case *vimtypes.VirtualDiskSeSparseBackingInfo: + tBack.KeyId = keyID + case *vimtypes.VirtualDiskSparseVer2BackingInfo: + tBack.KeyId = keyID + } + } + } + + return nil +} diff --git a/pkg/providers/vsphere/vmprovider.go b/pkg/providers/vsphere/vmprovider.go index 0d7d5966b..375e1cbeb 100644 --- a/pkg/providers/vsphere/vmprovider.go +++ b/pkg/providers/vsphere/vmprovider.go @@ -7,24 +7,32 @@ package vsphere import ( "context" "encoding/json" + "errors" "fmt" "math/rand" "strings" "sync" "sync/atomic" + "github.com/go-logr/logr" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/ovf" "github.com/vmware/govmomi/task" "github.com/vmware/govmomi/vapi/library" vimtypes "github.com/vmware/govmomi/vim25/types" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apierrorsutil "k8s.io/apimachinery/pkg/util/errors" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/yaml" imgregv1a1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha1" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + pkgcnd "github.com/vmware-tanzu/vm-operator/pkg/conditions" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" "github.com/vmware-tanzu/vm-operator/pkg/providers" @@ -146,34 +154,140 @@ func (vs *vSphereVMProvider) clearAndLogoutVcClient(ctx context.Context) { } } -// SyncVirtualMachineImage syncs the vmi object with the OVF Envelope retrieved from the cli object. -func (vs *vSphereVMProvider) SyncVirtualMachineImage(ctx context.Context, cli, vmi ctrlclient.Object) error { - var itemID, contentVersion string - var itemType imgregv1a1.ContentLibraryItemType +// ErrSyncVMICacheNotReady is returned from SyncVirtualMachineImage when the +// VirtualMachineImageCache object is not ready. +var ErrSyncVMICacheNotReady = errors.New("cache not ready") + +// SyncVirtualMachineImage syncs the vmi object with the OVF Envelope retrieved +// from the content library item object. +func (vs *vSphereVMProvider) SyncVirtualMachineImage( + ctx context.Context, + cli, + vmi ctrlclient.Object) error { + + var ( + itemID string + itemVersion string + itemType imgregv1a1.ContentLibraryItemType + ) switch cli := cli.(type) { case *imgregv1a1.ContentLibraryItem: itemID = string(cli.Spec.UUID) - contentVersion = cli.Status.ContentVersion + itemVersion = cli.Status.ContentVersion itemType = cli.Status.Type case *imgregv1a1.ClusterContentLibraryItem: itemID = string(cli.Spec.UUID) - contentVersion = cli.Status.ContentVersion + itemVersion = cli.Status.ContentVersion itemType = cli.Status.Type default: return fmt.Errorf("unexpected content library item K8s object type %T", cli) } - logger := log.V(4).WithValues("vmiName", vmi.GetName(), "cliName", cli.GetName()) + logger := log.V(4).WithValues( + "vmiName", vmi.GetName(), + "cliName", cli.GetName()) // Exit early if the library item type is not an OVF. if itemType != imgregv1a1.ContentLibraryItemTypeOvf { - logger.Info("Skip syncing VMI content as the library item is not OVF", - "libraryItemType", itemType) + logger.Info( + "Skip syncing VMI content as the library item is not OVF", + "itemType", itemType) return nil } - ovfEnvelope, err := ovfcache.GetOVFEnvelope(ctx, itemID, contentVersion) + if pkgcfg.FromContext(ctx).Features.FastDeploy { + return vs.syncVirtualMachineImageFastDeploy(ctx, vmi, logger, itemID, itemVersion) + } + return vs.syncVirtualMachineImage(ctx, vmi, itemID, itemVersion) +} + +func (vs *vSphereVMProvider) syncVirtualMachineImageFastDeploy( + ctx context.Context, + vmi ctrlclient.Object, + logger logr.Logger, + itemID, + itemVersion string) error { + + // Create or patch the VMICache object. + vmiCache := vmopv1.VirtualMachineImageCache{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkgcfg.FromContext(ctx).PodNamespace, + Name: vmi.GetName(), + }, + } + if _, err := controllerutil.CreateOrPatch( + ctx, + vs.k8sClient, + &vmiCache, + func() error { + vmiCache.Spec.ProviderID = itemID + vmiCache.Spec.ProviderVersion = itemVersion + return nil + }); err != nil { + return fmt.Errorf( + "failed to createOrPatch image cache resource: %w", err) + } + + // Check if the OVF data is ready. + c := pkgcnd.Get(vmiCache, vmopv1.VirtualMachineImageCacheConditionOVFReady) + switch { + case c != nil && c.Status == metav1.ConditionFalse: + + return fmt.Errorf("failed to get ovf: %s", c.Message) + + case c == nil, + c.Status != metav1.ConditionTrue, + vmiCache.Status.OVF == nil, + vmiCache.Status.OVF.ProviderVersion != itemVersion: + + logger.V(4).Info( + "Skip sync VMI", + "vmiCache.ovfCond", c, + "vmiCache.status.ovf", vmiCache.Status.OVF, + "expectedContentVersion", itemVersion) + + return ErrSyncVMICacheNotReady + } + + // Get the OVF data. + var ( + ovfConfigMap corev1.ConfigMap + ovfConfigMapKey = ctrlclient.ObjectKey{ + Namespace: vmiCache.Namespace, + Name: vmiCache.Status.OVF.ConfigMapName, + } + ) + if err := vs.k8sClient.Get( + ctx, + ovfConfigMapKey, + &ovfConfigMap); err != nil { + + if apierrors.IsNotFound(err) { + return ErrSyncVMICacheNotReady + } + + return fmt.Errorf("failed to get ovf configmap: %w", err) + } + + var ovfEnvelope ovf.Envelope + if err := yaml.Unmarshal( + []byte(ovfConfigMap.Data["value"]), &ovfEnvelope); err != nil { + + return fmt.Errorf("failed to unmarshal ovf yaml into envelope: %w", err) + } + + contentlibrary.UpdateVmiWithOvfEnvelope(vmi, ovfEnvelope) + return nil +} + +func (vs *vSphereVMProvider) syncVirtualMachineImage( + ctx context.Context, + vmi ctrlclient.Object, + itemID, + itemVersion string) error { + + ovfEnvelope, err := ovfcache.GetOVFEnvelope(ctx, itemID, itemVersion) if err != nil { return fmt.Errorf("failed to get OVF envelope for library item %q: %w", itemID, err) } diff --git a/pkg/providers/vsphere/vmprovider_test.go b/pkg/providers/vsphere/vmprovider_test.go index 9bfa446cf..03a463a28 100644 --- a/pkg/providers/vsphere/vmprovider_test.go +++ b/pkg/providers/vsphere/vmprovider_test.go @@ -5,17 +5,21 @@ package vsphere_test import ( + "errors" "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/vmware/govmomi/vapi/library" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" imgregv1a1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha1" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" "github.com/vmware-tanzu/vm-operator/pkg/providers" vsphere "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" "github.com/vmware-tanzu/vm-operator/test/builder" @@ -51,7 +55,7 @@ func cpuFreqTests() { }) } -func syncVirtualMachineImageTests() { +var _ = Describe("SyncVirtualMachineImage", func() { var ( ctx *builder.TestContextForVCSim testConfig builder.VCSimTestConfig @@ -77,7 +81,7 @@ func syncVirtualMachineImageTests() { }) When("content library item is not an OVF type", func() { - It("should exit early without updating VM Image status", func() { + It("should return early without updating VM Image status", func() { isoItem := &imgregv1a1.ContentLibraryItem{ Status: imgregv1a1.ContentLibraryItemStatus{ Type: imgregv1a1.ContentLibraryItemTypeIso, @@ -89,73 +93,355 @@ func syncVirtualMachineImageTests() { }) }) - When("content library item is an OVF type but it fails to get the OVF envelope", func() { - It("should return an error", func() { - ovfItem := &imgregv1a1.ContentLibraryItem{ - Spec: imgregv1a1.ContentLibraryItemSpec{ - // Use an invalid item ID to fail to get the OVF envelope. - UUID: "invalid-library-ID", - }, - Status: imgregv1a1.ContentLibraryItemStatus{ - Type: imgregv1a1.ContentLibraryItemTypeOvf, - }, - } - err := vmProvider.SyncVirtualMachineImage(ctx, ovfItem, &vmopv1.VirtualMachineImage{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to get OVF envelope for library item \"invalid-library-ID\"")) - }) - }) + When("content library item is an OVF type", func() { + // TODO(akutz) Promote this block when the FSS WCP_VMService_FastDeploy is + // removed. + When("FSS WCP_VMService_FastDeploy is enabled", func() { - When("content library item is an OVF type but the OVF envelope is nil", func() { - It("should return an error", func() { - libraryItem := library.Item{ - Name: "test-image-ovf-empty", - // Use a non-OVF type here to cause the retrieved OVF envelope to be nil. - Type: "empty", - LibraryID: ctx.ContentLibraryID, - } - itemID := builder.CreateContentLibraryItem( - ctx, - library.NewManager(ctx.RestClient), - libraryItem, - "", + var ( + err error + cli imgregv1a1.ContentLibraryItem + vmi vmopv1.VirtualMachineImage + vmic vmopv1.VirtualMachineImageCache + vmicm corev1.ConfigMap + createVMIC bool ) - Expect(itemID).NotTo(BeEmpty()) - ovfItem := &imgregv1a1.ContentLibraryItem{ - Spec: imgregv1a1.ContentLibraryItemSpec{ - UUID: types.UID(itemID), - }, - Status: imgregv1a1.ContentLibraryItemStatus{ - Type: imgregv1a1.ContentLibraryItemTypeOvf, - }, + BeforeEach(func() { + pkgcfg.UpdateContext(ctx, func(config *pkgcfg.Config) { + config.Features.FastDeploy = true + }) + + cli = imgregv1a1.ContentLibraryItem{ + Spec: imgregv1a1.ContentLibraryItemSpec{ + UUID: types.UID(ctx.ContentLibraryItemID), + }, + Status: imgregv1a1.ContentLibraryItemStatus{ + ContentVersion: "v1", + Type: imgregv1a1.ContentLibraryItemTypeOvf, + }, + } + + vmi = vmopv1.VirtualMachineImage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + Name: "my-vmi", + }, + } + + createVMIC = true + vmic = vmopv1.VirtualMachineImageCache{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkgcfg.FromContext(ctx).PodNamespace, + Name: vmi.Name, + }, + Status: vmopv1.VirtualMachineImageCacheStatus{ + OVF: &vmopv1.VirtualMachineImageCacheOVFStatus{ + ConfigMapName: vmi.Name, + ProviderVersion: "v1", + }, + Conditions: []metav1.Condition{ + { + Type: vmopv1.VirtualMachineImageCacheConditionOVFReady, + Status: metav1.ConditionTrue, + }, + }, + }, + } + + vmicm = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: vmic.Namespace, + Name: vmic.Name, + }, + Data: map[string]string{ + "value": ovfEnvelopeYAML, + }, + } + Expect(ctx.Client.Create(ctx, &vmicm)).To(Succeed()) + }) + + JustBeforeEach(func() { + if createVMIC { + status := vmic.Status.DeepCopy() + Expect(ctx.Client.Create(ctx, &vmic)).To(Succeed()) + vmic.Status = *status + Expect(ctx.Client.Status().Update(ctx, &vmic)).To(Succeed()) + } + err = vmProvider.SyncVirtualMachineImage(ctx, &cli, &vmi) + }) + + When("it fails to createOrPatch the VMICache resource", func() { + // TODO(akutz) Add interceptors to the vcSim test context so + // this can be tested. + XIt("should return an error", func() { + + }) + }) + + assertVMICExists := func(namespace, name string) { + var ( + obj vmopv1.VirtualMachineImageCache + key = ctrlclient.ObjectKey{ + Namespace: namespace, + Name: name, + } + ) + Expect(ctx.Client.Get(ctx, key, &obj)).To(Succeed()) } - err := vmProvider.SyncVirtualMachineImage(ctx, ovfItem, &vmopv1.VirtualMachineImage{}) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("OVF envelope is nil for library item %q", itemID))) + + When("OVF condition is False", func() { + BeforeEach(func() { + vmic.Status.Conditions[0].Status = metav1.ConditionFalse + vmic.Status.Conditions[0].Message = "fubar" + }) + It("should return an error", func() { + Expect(err).To(MatchError("failed to get ovf: fubar")) + }) + }) + + When("OVF is not ready", func() { + When("condition is missing", func() { + BeforeEach(func() { + createVMIC = false + }) + It("should return ErrSyncVMICacheNotReady", func() { + assertVMICExists(vmic.Namespace, vmic.Name) + Expect(errors.Is(err, vsphere.ErrSyncVMICacheNotReady)).To(BeTrue()) + }) + }) + When("condition is unknown", func() { + BeforeEach(func() { + vmic.Status.Conditions[0].Status = metav1.ConditionUnknown + }) + It("should return ErrSyncVMICacheNotReady", func() { + Expect(errors.Is(err, vsphere.ErrSyncVMICacheNotReady)).To(BeTrue()) + }) + }) + When("status.ovf is nil", func() { + BeforeEach(func() { + vmic.Status.OVF = nil + }) + It("should return ErrSyncVMICacheNotReady", func() { + Expect(errors.Is(err, vsphere.ErrSyncVMICacheNotReady)).To(BeTrue()) + }) + }) + When("status.ovf.providerVersion does not match expected version", func() { + BeforeEach(func() { + vmic.Status.OVF.ProviderVersion = "" + }) + It("should return ErrSyncVMICacheNotReady", func() { + Expect(errors.Is(err, vsphere.ErrSyncVMICacheNotReady)).To(BeTrue()) + }) + }) + When("configmap is missing", func() { + BeforeEach(func() { + Expect(ctx.Client.Delete(ctx, &vmicm)).To(Succeed()) + }) + It("should return ErrSyncVMICacheNotReady", func() { + Expect(errors.Is(err, vsphere.ErrSyncVMICacheNotReady)).To(BeTrue()) + }) + }) + }) + + When("OVF is ready", func() { + When("marshaled ovf data is invalid", func() { + BeforeEach(func() { + vmicm.Data["value"] = "invalid" + Expect(ctx.Client.Update(ctx, &vmicm)).To(Succeed()) + }) + It("should return an error", func() { + Expect(err).To(MatchError("failed to unmarshal ovf yaml into envelope: " + + "error unmarshaling JSON: while decoding JSON: " + + "json: cannot unmarshal string into Go value of type ovf.Envelope")) + }) + }) + When("marshaled ovf data is valid", func() { + It("should return success and update VM Image status accordingly", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(vmi.Status.Firmware).To(Equal("efi")) + Expect(vmi.Status.HardwareVersion).NotTo(BeNil()) + Expect(*vmi.Status.HardwareVersion).To(Equal(int32(9))) + Expect(vmi.Status.OSInfo.ID).To(Equal("36")) + Expect(vmi.Status.OSInfo.Type).To(Equal("otherLinuxGuest")) + Expect(vmi.Status.Disks).To(HaveLen(1)) + Expect(vmi.Status.Disks[0].Capacity.String()).To(Equal("30Mi")) + Expect(vmi.Status.Disks[0].Size.String()).To(Equal("18304Ki")) + }) + }) + + }) }) - }) - When("content library item is an OVF type with valid OVF envelope", func() { - It("should return success and update VM Image status accordingly", func() { - ovfItem := &imgregv1a1.ContentLibraryItem{ - Spec: imgregv1a1.ContentLibraryItemSpec{ - UUID: types.UID(ctx.ContentLibraryItemID), - }, - Status: imgregv1a1.ContentLibraryItemStatus{ - Type: imgregv1a1.ContentLibraryItemTypeOvf, - }, - } - var vmi vmopv1.VirtualMachineImage - Expect(vmProvider.SyncVirtualMachineImage(ctx, ovfItem, &vmi)).To(Succeed()) - Expect(vmi.Status.Firmware).To(Equal("efi")) - Expect(vmi.Status.HardwareVersion).NotTo(BeNil()) - Expect(*vmi.Status.HardwareVersion).To(Equal(int32(9))) - Expect(vmi.Status.OSInfo.ID).To(Equal("36")) - Expect(vmi.Status.OSInfo.Type).To(Equal("otherLinuxGuest")) - Expect(vmi.Status.Disks).To(HaveLen(1)) - Expect(vmi.Status.Disks[0].Capacity.String()).To(Equal("30Mi")) - Expect(vmi.Status.Disks[0].Size.String()).To(Equal("18304Ki")) + // TODO(akutz) Remove this block when the FSS WCP_VMService_FastDeploy is + // removed. + When("FSS WCP_VMService_FastDeploy is disabled", func() { + + BeforeEach(func() { + pkgcfg.UpdateContext(ctx, func(config *pkgcfg.Config) { + config.Features.FastDeploy = false + }) + }) + + When("it fails to get the OVF envelope", func() { + It("should return an error", func() { + cli := &imgregv1a1.ContentLibraryItem{ + Spec: imgregv1a1.ContentLibraryItemSpec{ + // Use an invalid item ID to fail to get the OVF envelope. + UUID: "invalid-library-ID", + }, + Status: imgregv1a1.ContentLibraryItemStatus{ + Type: imgregv1a1.ContentLibraryItemTypeOvf, + }, + } + err := vmProvider.SyncVirtualMachineImage(ctx, cli, &vmopv1.VirtualMachineImage{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get OVF envelope for library item \"invalid-library-ID\"")) + }) + }) + + When("OVF envelope is nil", func() { + It("should return an error", func() { + ovfItem := &imgregv1a1.ContentLibraryItem{ + Spec: imgregv1a1.ContentLibraryItemSpec{ + UUID: types.UID(ctx.ContentLibraryIsoItemID), + }, + Status: imgregv1a1.ContentLibraryItemStatus{ + Type: imgregv1a1.ContentLibraryItemTypeOvf, + }, + } + err := vmProvider.SyncVirtualMachineImage(ctx, ovfItem, &vmopv1.VirtualMachineImage{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("OVF envelope is nil for library item %q", ctx.ContentLibraryIsoItemID))) + }) + }) + + When("there is a valid OVF envelope", func() { + It("should return success and update VM Image status accordingly", func() { + cli := &imgregv1a1.ContentLibraryItem{ + Spec: imgregv1a1.ContentLibraryItemSpec{ + UUID: types.UID(ctx.ContentLibraryItemID), + }, + Status: imgregv1a1.ContentLibraryItemStatus{ + Type: imgregv1a1.ContentLibraryItemTypeOvf, + }, + } + var vmi vmopv1.VirtualMachineImage + Expect(vmProvider.SyncVirtualMachineImage(ctx, cli, &vmi)).To(Succeed()) + Expect(vmi.Status.Firmware).To(Equal("efi")) + Expect(vmi.Status.HardwareVersion).NotTo(BeNil()) + Expect(*vmi.Status.HardwareVersion).To(Equal(int32(9))) + Expect(vmi.Status.OSInfo.ID).To(Equal("36")) + Expect(vmi.Status.OSInfo.Type).To(Equal("otherLinuxGuest")) + Expect(vmi.Status.Disks).To(HaveLen(1)) + Expect(vmi.Status.Disks[0].Capacity.String()).To(Equal("30Mi")) + Expect(vmi.Status.Disks[0].Size.String()).To(Equal("18304Ki")) + }) + }) }) }) -} +}) + +const ovfEnvelopeYAML = ` +diskSection: + disk: + - capacity: "30" + capacityAllocationUnits: byte * 2^20 + diskId: vmdisk1 + fileRef: file1 + format: http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized + populatedSize: 18743296 + info: Virtual disk information +networkSection: + info: The list of logical networks + network: + - description: The nat network + name: nat +references: +- href: ttylinux-pc_i486-16.1-disk1.vmdk + id: file1 + size: 10595840 +virtualSystem: + id: vm + info: A virtual machine + name: ttylinux-pc_i486-16.1 + operatingSystemSection: + id: 36 + info: The kind of installed guest operating system + osType: otherLinuxGuest + virtualHardwareSection: + - config: + - key: firmware + required: false + value: efi + - key: powerOpInfo.powerOffType + required: false + value: soft + - key: powerOpInfo.resetType + required: false + value: soft + - key: powerOpInfo.suspendType + required: false + value: soft + - key: tools.syncTimeWithHost + required: false + value: "true" + - key: tools.toolsUpgradePolicy + required: false + value: upgradeAtPowerCycle + id: null + info: Virtual hardware requirements + item: + - allocationUnits: hertz * 10^6 + description: Number of Virtual CPUs + elementName: 1 virtual CPU(s) + instanceID: "1" + resourceType: 3 + virtualQuantity: 1 + - allocationUnits: byte * 2^20 + description: Memory Size + elementName: 32MB of memory + instanceID: "2" + resourceType: 4 + virtualQuantity: 32 + - address: "0" + description: IDE Controller + elementName: ideController0 + instanceID: "3" + resourceType: 5 + - addressOnParent: "0" + elementName: disk0 + hostResource: + - ovf:/disk/vmdisk1 + instanceID: "4" + parent: "3" + resourceType: 17 + - addressOnParent: "1" + automaticAllocation: true + config: + - key: wakeOnLanEnabled + required: false + value: "false" + connection: + - nat + description: E1000 ethernet adapter on "nat" + elementName: ethernet0 + instanceID: "5" + resourceSubType: E1000 + resourceType: 10 + - automaticAllocation: false + elementName: video + instanceID: "6" + required: false + resourceType: 24 + - automaticAllocation: false + elementName: vmci + instanceID: "7" + required: false + resourceSubType: vmware.vmci + resourceType: 1 + system: + elementName: Virtual Hardware Family + instanceID: "0" + virtualSystemIdentifier: ttylinux-pc_i486-16.1 + virtualSystemType: vmx-09` diff --git a/pkg/providers/vsphere/vmprovider_vm.go b/pkg/providers/vsphere/vmprovider_vm.go index c46e0c80c..47e97d706 100644 --- a/pkg/providers/vsphere/vmprovider_vm.go +++ b/pkg/providers/vsphere/vmprovider_vm.go @@ -31,8 +31,9 @@ import ( vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" "github.com/vmware-tanzu/vm-operator/api/v1alpha3/common" - "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcnd "github.com/vmware-tanzu/vm-operator/pkg/conditions" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" ctxop "github.com/vmware-tanzu/vm-operator/pkg/context/operation" "github.com/vmware-tanzu/vm-operator/pkg/providers" @@ -545,6 +546,9 @@ func (vs *vSphereVMProvider) getCreateArgs( } if pkgcfg.FromContext(vmCtx).Features.FastDeploy { + if err := vs.vmCreateGetSourceDiskPaths(vmCtx, vcClient, createArgs); err != nil { + return nil, err + } if err := vs.vmCreatePathNameFromDatastoreRecommendation(vmCtx, createArgs); err != nil { return nil, err } @@ -576,19 +580,18 @@ func (vs *vSphereVMProvider) createVirtualMachine( vcClient.RestClient(), vcClient.VimClient(), vcClient.Finder(), - vcClient.Datacenter(), &args.CreateArgs) if err != nil { ctx.Logger.Error(err, "CreateVirtualMachine failed") - conditions.MarkFalse( + pkgcnd.MarkFalse( ctx.VM, vmopv1.VirtualMachineConditionCreated, "Error", err.Error()) if pkgcfg.FromContext(ctx).Features.FastDeploy { - conditions.MarkFalse( + pkgcnd.MarkFalse( ctx.VM, vmopv1.VirtualMachineConditionPlacementReady, "Error", @@ -599,7 +602,7 @@ func (vs *vSphereVMProvider) createVirtualMachine( } ctx.VM.Status.UniqueID = moRef.Reference().Value - conditions.MarkTrue(ctx.VM, vmopv1.VirtualMachineConditionCreated) + pkgcnd.MarkTrue(ctx.VM, vmopv1.VirtualMachineConditionCreated) if pkgcfg.FromContext(ctx).Features.FastDeploy { if zoneName := args.ZoneName; zoneName != "" { @@ -631,7 +634,6 @@ func (vs *vSphereVMProvider) createVirtualMachineAsync( vcClient.RestClient(), vcClient.VimClient(), vcClient.Finder(), - vcClient.Datacenter(), &args.CreateArgs) if vimErr != nil { @@ -646,14 +648,14 @@ func (vs *vSphereVMProvider) createVirtualMachineAsync( func() error { if vimErr != nil { - conditions.MarkFalse( + pkgcnd.MarkFalse( ctx.VM, vmopv1.VirtualMachineConditionCreated, "Error", vimErr.Error()) if pkgcfg.FromContext(ctx).Features.FastDeploy { - conditions.MarkFalse( + pkgcnd.MarkFalse( ctx.VM, vmopv1.VirtualMachineConditionPlacementReady, "Error", @@ -673,7 +675,7 @@ func (vs *vSphereVMProvider) createVirtualMachineAsync( } ctx.VM.Status.UniqueID = moRef.Reference().Value - conditions.MarkTrue(ctx.VM, vmopv1.VirtualMachineConditionCreated) + pkgcnd.MarkTrue(ctx.VM, vmopv1.VirtualMachineConditionCreated) return nil }, @@ -812,7 +814,7 @@ func (vs *vSphereVMProvider) vmCreateDoPlacement( defer func() { if retErr != nil { - conditions.MarkFalse( + pkgcnd.MarkFalse( vmCtx.VM, vmopv1.VirtualMachineConditionPlacementReady, "NotReady", @@ -860,6 +862,7 @@ func (vs *vSphereVMProvider) vmCreateDoPlacement( } if pkgcfg.FromContext(vmCtx).Features.FastDeploy { + createArgs.DatacenterMoID = vcClient.Datacenter().Reference().Value createArgs.Datastores = make([]vmlifecycle.DatastoreRef, len(result.Datastores)) for i := range result.Datastores { createArgs.Datastores[i].DiskKey = result.Datastores[i].DiskKey @@ -903,7 +906,7 @@ func (vs *vSphereVMProvider) vmCreateDoPlacement( } } - conditions.MarkTrue(vmCtx.VM, vmopv1.VirtualMachineConditionPlacementReady) + pkgcnd.MarkTrue(vmCtx.VM, vmopv1.VirtualMachineConditionPlacementReady) return nil } @@ -974,6 +977,76 @@ func (vs *vSphereVMProvider) vmCreateGetFolderAndRPMoIDs( return nil } +// vmCreateGetSourceDiskPaths gets paths to the source disk(s) used to create +// the VM. +func (vs *vSphereVMProvider) vmCreateGetSourceDiskPaths( + vmCtx pkgctx.VirtualMachineContext, + _ *vcclient.Client, + createArgs *VMCreateArgs) error { + + if len(createArgs.Datastores) == 0 { + return errors.New("no compatible datastores") + } + + var ( + datacenterID = vs.vcClient.Datacenter().Reference().Value + datastoreID = createArgs.Datastores[0].MoRef.Value + ) + + // Create/patch/get the VirtualMachineImageCache resource. + obj := vmopv1.VirtualMachineImageCache{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkgcfg.FromContext(vmCtx).PodNamespace, + Name: createArgs.ImageObj.GetName(), + }, + } + if _, err := controllerutil.CreateOrPatch( + vmCtx, + vs.k8sClient, + &obj, + func() error { + obj.Spec.ProviderID = createArgs.ImageStatus.ProviderItemID + obj.Spec.ProviderVersion = createArgs.ImageStatus.ProviderContentVersion + obj.Spec.SetLocation(datacenterID, datastoreID) + return nil + }); err != nil { + return fmt.Errorf( + "failed to createOrPatch image cache resource: %w", err) + } + + // Check if the disks are cached. + for i := range obj.Status.Locations { + l := obj.Status.Locations[i] + if l.DatacenterID == datacenterID && l.DatastoreID == datastoreID { + if c := pkgcnd.Get(l, vmopv1.ReadyConditionType); c != nil { + switch c.Status { + case metav1.ConditionTrue: + // The location has the cached disks. + createArgs.DiskPaths = l.Files + vmCtx.Logger.Info("got source disks", "disks", l.Files) + + // Make sure the VM is no longer reconciled when the VMI + // Cache resource is updated. + delete(vmCtx.VM.Labels, pkgconst.VMICacheLabelKey) + + return nil + case metav1.ConditionFalse: + // The disks could not be cached at that location. + return fmt.Errorf("failed to cache disks: %s", c.Message) + } + } + } + } + + // If the disks are not yet cached, add a label to the VM that will cause + // it to be reconciled when the VMICache resource is updated. + // + // This label is removed once the disks are available. + vmCtx.VM.Labels[pkgconst.VMICacheLabelKey] = createArgs.ImageObj.GetName() + + return errors.New("disks not yet available") +} + func (vs *vSphereVMProvider) vmCreateFixupConfigSpec( vmCtx pkgctx.VirtualMachineContext, vcClient *vcclient.Client, @@ -1161,7 +1234,7 @@ func (vs *vSphereVMProvider) vmCreateGetVirtualMachineImage( default: if !SkipVMImageCLProviderCheck { err := fmt.Errorf("unsupported image provider kind: %s", providerRef.Kind) - conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionImageReady, "NotSupported", err.Error()) + pkgcnd.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionImageReady, "NotSupported", err.Error()) return err } // Testing only: we'll clone the source VM found in the Inventory. @@ -1233,20 +1306,20 @@ func (vs *vSphereVMProvider) vmCreateGetStoragePrereqs( // This will be true in WCP. if cfg.StorageClassRequired { err := fmt.Errorf("StorageClass is required but not specified") - conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, "StorageClassRequired", err.Error()) + pkgcnd.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, "StorageClassRequired", err.Error()) return err } // Testing only for standalone gce2e. if cfg.Datastore == "" { err := fmt.Errorf("no Datastore provided in configuration") - conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, "DatastoreNotFound", err.Error()) + pkgcnd.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, "DatastoreNotFound", err.Error()) return err } datastore, err := vcClient.Finder().Datastore(vmCtx, cfg.Datastore) if err != nil { - conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, "DatastoreNotFound", err.Error()) + pkgcnd.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, "DatastoreNotFound", err.Error()) return fmt.Errorf("failed to find Datastore %s: %w", cfg.Datastore, err) } @@ -1256,7 +1329,7 @@ func (vs *vSphereVMProvider) vmCreateGetStoragePrereqs( vmStorage, err := storage.GetVMStorageData(vmCtx, vs.k8sClient) if err != nil { reason, msg := errToConditionReasonAndMessage(err) - conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, reason, msg) + pkgcnd.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, reason, msg) return err } @@ -1264,14 +1337,14 @@ func (vs *vSphereVMProvider) vmCreateGetStoragePrereqs( provisioningType, err := virtualmachine.GetDefaultDiskProvisioningType(vmCtx, vcClient, vmStorageProfileID) if err != nil { reason, msg := errToConditionReasonAndMessage(err) - conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, reason, msg) + pkgcnd.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady, reason, msg) return err } createArgs.Storage = vmStorage createArgs.StorageProvisioning = provisioningType createArgs.StorageProfileID = vmStorageProfileID - conditions.MarkTrue(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady) + pkgcnd.MarkTrue(vmCtx.VM, vmopv1.VirtualMachineConditionStorageReady) return nil } @@ -1283,7 +1356,7 @@ func (vs *vSphereVMProvider) vmCreateDoNetworking( networkSpec := vmCtx.VM.Spec.Network if networkSpec == nil || networkSpec.Disabled { - conditions.Delete(vmCtx.VM, vmopv1.VirtualMachineConditionNetworkReady) + pkgcnd.Delete(vmCtx.VM, vmopv1.VirtualMachineConditionNetworkReady) return nil } @@ -1295,12 +1368,12 @@ func (vs *vSphereVMProvider) vmCreateDoNetworking( nil, // Don't know the CCR yet (needed to resolve backings for NSX-T) networkSpec) if err != nil { - conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionNetworkReady, "NotReady", err.Error()) + pkgcnd.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionNetworkReady, "NotReady", err.Error()) return err } createArgs.NetworkResults = results - conditions.MarkTrue(vmCtx.VM, vmopv1.VirtualMachineConditionNetworkReady) + pkgcnd.MarkTrue(vmCtx.VM, vmopv1.VirtualMachineConditionNetworkReady) return nil } diff --git a/pkg/providers/vsphere/vmprovider_vm_utils.go b/pkg/providers/vsphere/vmprovider_vm_utils.go index 89bcaa58f..50d782f92 100644 --- a/pkg/providers/vsphere/vmprovider_vm_utils.go +++ b/pkg/providers/vsphere/vmprovider_vm_utils.go @@ -243,11 +243,15 @@ func GetVirtualMachineImageSpecAndStatus( } vmiNotReadyMessage := "VirtualMachineImage is not ready" + + // Mirror the image's ReadyConditionType into the VM's + // VirtualMachineConditionImageReady. conditions.SetMirror( vmCtx.VM, vmopv1.VirtualMachineConditionImageReady, obj.(conditions.Getter), conditions.WithFallbackValue(false, "NotReady", vmiNotReadyMessage)) + if conditions.IsFalse(vmCtx.VM, vmopv1.VirtualMachineConditionImageReady) { return nil, vmopv1.VirtualMachineImageSpec{}, diff --git a/pkg/providers/vsphere/vsphere_suite_test.go b/pkg/providers/vsphere/vsphere_suite_test.go index 009ae6490..b0f72d76b 100644 --- a/pkg/providers/vsphere/vsphere_suite_test.go +++ b/pkg/providers/vsphere/vsphere_suite_test.go @@ -25,7 +25,6 @@ var suite = builder.NewTestSuite() func vcSimTests() { Describe("CPUFreq", cpuFreqTests) - Describe("SyncVirtualMachineImage", syncVirtualMachineImageTests) Describe("ResourcePolicyTests", resourcePolicyTests) Describe("VirtualMachine", vmTests) Describe("VirtualMachineE2E", vmE2ETests) diff --git a/pkg/util/vmopv1/image.go b/pkg/util/vmopv1/image.go index c54addae0..78aa17d32 100644 --- a/pkg/util/vmopv1/image.go +++ b/pkg/util/vmopv1/image.go @@ -11,11 +11,18 @@ import ( "path" "strings" + "github.com/go-logr/logr" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" imgregv1a1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha1" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" - "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgcnd "github.com/vmware-tanzu/vm-operator/pkg/conditions" + pkgconst "github.com/vmware-tanzu/vm-operator/pkg/constants" pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" ) @@ -132,10 +139,10 @@ func GetImage( } func IsImageReady(img vmopv1.VirtualMachineImage) error { - if !conditions.IsTrue(&img, vmopv1.ReadyConditionType) { + if !pkgcnd.IsTrue(&img, vmopv1.ReadyConditionType) { return fmt.Errorf( "image condition is not ready: %v", - conditions.Get(&img, vmopv1.ReadyConditionType)) + pkgcnd.Get(&img, vmopv1.ReadyConditionType)) } if img.Spec.ProviderRef == nil || img.Spec.ProviderRef.Name == "" { return errors.New("image provider ref is empty") @@ -231,3 +238,107 @@ func GetStorageURIsForLibraryItemDisks( } return storageURIs, nil } + +// VirtualMachineImageCacheToItemMapper returns a mapper function used to +// enqueue reconcile requests for resources in response to an event on +// a VirtualMachineImageCache resource. +func VirtualMachineImageCacheToItemMapper( + ctx context.Context, + k8sClient ctrlclient.Client, + groupVersion schema.GroupVersion, + kind string) handler.MapFunc { + + if ctx == nil { + panic("context is nil") + } + if k8sClient == nil { + panic("k8sClient is nil") + } + if groupVersion.Empty() { + panic("groupVersion is empty") + } + if kind == "" { + panic("kind is empty") + } + + gvkString := groupVersion.WithKind(kind).String() + listGVK := groupVersion.WithKind(kind + "List") + + // For a given VirtualMachineImageCache, return reconcile requests for + // resources that have the label pkgconst.VMICacheLabelKey with a value set + // to the name of a VMI cache resource. + return func(ctx context.Context, o ctrlclient.Object) []reconcile.Request { + if ctx == nil { + panic("context is nil") + } + if o == nil { + panic("object is nil") + } + obj, ok := o.(*vmopv1.VirtualMachineImageCache) + if !ok { + panic(fmt.Sprintf("object is %T", o)) + } + + // Do not reconcile anything if the referred object is being deleted. + if obj.DeletionTimestamp != nil && !obj.DeletionTimestamp.IsZero() { + return nil + } + + // Do not reconcile anything unless the OVF for this VMI Cache resource + // is ready. + if !pkgcnd.IsTrue(obj, vmopv1.VirtualMachineImageCacheConditionOVFReady) { + return nil + } + + logger := logr.FromContextOrDiscard(ctx). + WithValues("name", o.GetName(), + "namespace", o.GetNamespace()) + logger.V(4).Info( + "Reconciling all resources referencing an VirtualMachineImageCache", + "resourceGVK", gvkString) + + list := unstructured.UnstructuredList{ + Object: map[string]interface{}{}, + } + list.SetGroupVersionKind(listGVK) + + // Find all the referrers. + if err := k8sClient.List( + ctx, + &list, + ctrlclient.MatchingLabels(map[string]string{ + pkgconst.VMICacheLabelKey: obj.Name, + })); err != nil { + + if !apierrors.IsNotFound(err) { + logger.Error( + err, + "Failed to list resources due to VirtualMachineImageCache watch", + "resourceGVK", gvkString) + } + return nil + } + + // Populate reconcile requests for referrers. + var requests []reconcile.Request + for i := range list.Items { + requests = append( + requests, + reconcile.Request{ + NamespacedName: ctrlclient.ObjectKey{ + Namespace: list.Items[i].GetNamespace(), + Name: list.Items[i].GetName(), + }, + }) + } + + if len(requests) > 0 { + logger.V(4).Info( + "Reconciling resources due to VirtualMachineImageCache watch", + "resourceGVK", gvkString, + "requests", requests) + } + + return requests + } +} diff --git a/pkg/util/vmopv1/image_test.go b/pkg/util/vmopv1/image_test.go index 62f4a5aa7..5e7f18ca5 100644 --- a/pkg/util/vmopv1/image_test.go +++ b/pkg/util/vmopv1/image_test.go @@ -6,19 +6,27 @@ package vmopv1_test import ( "context" + "errors" "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" imgregv1a1 "github.com/vmware-tanzu/image-registry-operator-api/api/v1alpha1" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" vmopv1common "github.com/vmware-tanzu/vm-operator/api/v1alpha3/common" vmopv1util "github.com/vmware-tanzu/vm-operator/pkg/util/vmopv1" + "github.com/vmware-tanzu/vm-operator/test/builder" ) var _ = DescribeTable("IsImageReady", @@ -305,3 +313,330 @@ var _ = XDescribe("GetImage", func() { // TODO(akutz) Implement test }) + +var _ = Describe("VirtualMachineImageCacheToItemMapper", func() { + const ( + vmiCacheName = "my-vmi" + namespaceName = "fake" + ) + + var ( + ctx context.Context + k8sClient ctrlclient.Client + groupVersion schema.GroupVersion + kind string + withObjs []ctrlclient.Object + withFuncs interceptor.Funcs + obj *vmopv1.VirtualMachineImageCache + mapFn handler.MapFunc + mapFnCtx context.Context + mapFnObj ctrlclient.Object + reqs []reconcile.Request + ) + BeforeEach(func() { + reqs = nil + withObjs = nil + withFuncs = interceptor.Funcs{} + + ctx = context.Background() + mapFnCtx = ctx + groupVersion = schema.GroupVersion{ + Group: "", + Version: "v1", + } + kind = "ConfigMap" + + obj = &vmopv1.VirtualMachineImageCache{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmiCacheName, + Namespace: namespaceName, + }, + Status: vmopv1.VirtualMachineImageCacheStatus{ + Conditions: []metav1.Condition{ + { + Type: vmopv1.VirtualMachineImageCacheConditionOVFReady, + Status: metav1.ConditionTrue, + }, + }, + }, + } + mapFnObj = obj + }) + JustBeforeEach(func() { + withObjs = append(withObjs, obj) + k8sClient = builder.NewFakeClientWithInterceptors(withFuncs, withObjs...) + Expect(k8sClient).ToNot(BeNil()) + }) + When("panic is expected", func() { + When("ctx is nil", func() { + JustBeforeEach(func() { + ctx = nil + }) + It("should panic", func() { + Expect(func() { + _ = vmopv1util.VirtualMachineImageCacheToItemMapper( + ctx, + k8sClient, + groupVersion, + kind) + }).To(PanicWith("context is nil")) + }) + }) + When("k8sClient is nil", func() { + JustBeforeEach(func() { + k8sClient = nil + }) + It("should panic", func() { + Expect(func() { + _ = vmopv1util.VirtualMachineImageCacheToItemMapper( + ctx, + k8sClient, + groupVersion, + kind) + }).To(PanicWith("k8sClient is nil")) + }) + }) + When("groupVersion is empty", func() { + JustBeforeEach(func() { + groupVersion = schema.GroupVersion{} + }) + It("should panic", func() { + Expect(func() { + _ = vmopv1util.VirtualMachineImageCacheToItemMapper( + ctx, + k8sClient, + groupVersion, + kind) + }).To(PanicWith("groupVersion is empty")) + }) + }) + When("kind is empty", func() { + JustBeforeEach(func() { + kind = "" + }) + It("should panic", func() { + Expect(func() { + _ = vmopv1util.VirtualMachineImageCacheToItemMapper( + ctx, + k8sClient, + groupVersion, + kind) + }).To(PanicWith("kind is empty")) + }) + }) + Context("mapFn", func() { + JustBeforeEach(func() { + mapFn = vmopv1util.VirtualMachineImageCacheToItemMapper( + ctx, + k8sClient, + groupVersion, + kind) + Expect(mapFn).ToNot(BeNil()) + }) + When("ctx is nil", func() { + BeforeEach(func() { + mapFnCtx = nil + }) + It("should panic", func() { + Expect(func() { + _ = mapFn(mapFnCtx, mapFnObj) + }).To(PanicWith("context is nil")) + }) + }) + When("object is nil", func() { + BeforeEach(func() { + mapFnObj = nil + }) + It("should panic", func() { + Expect(func() { + _ = mapFn(mapFnCtx, mapFnObj) + }).To(PanicWith("object is nil")) + }) + }) + When("object is invalid", func() { + BeforeEach(func() { + mapFnObj = &vmopv1.VirtualMachine{} + }) + It("should panic", func() { + Expect(func() { + _ = mapFn(mapFnCtx, mapFnObj) + }).To(PanicWith(fmt.Sprintf("object is %T", mapFnObj))) + }) + }) + }) + }) + When("panic is not expected", func() { + JustBeforeEach(func() { + mapFn = vmopv1util.VirtualMachineImageCacheToItemMapper( + ctx, + k8sClient, + groupVersion, + kind) + Expect(mapFn).ToNot(BeNil()) + reqs = mapFn(mapFnCtx, mapFnObj) + }) + When("there is an error listing resources", func() { + BeforeEach(func() { + withFuncs.List = func( + ctx context.Context, + client ctrlclient.WithWatch, + list ctrlclient.ObjectList, + opts ...ctrlclient.ListOption) error { + + if _, ok := list.(*corev1.ConfigMapList); ok { + return errors.New("fake") + } + return client.List(ctx, list, opts...) + } + }) + Specify("no reconcile requests should be returned", func() { + Expect(reqs).To(BeEmpty()) + }) + }) + When("there are no matching resources", func() { + BeforeEach(func() { + withObjs = append(withObjs, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "configmap-1", + }, + }, + ) + }) + Specify("no reconcile requests should be returned", func() { + Expect(reqs).To(BeEmpty()) + }) + }) + When("there is a single matching resource but ovf is not ready", func() { + BeforeEach(func() { + obj.Status.Conditions = nil + + withObjs = append(withObjs, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "configmap-1", + Labels: map[string]string{ + "vmoperator.vmware.com/vmi-cache": vmiCacheName, + }, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-2", + Name: "configmap-2", + }, + }, + ) + }) + Specify("no reconcile requests should be returned", func() { + Expect(reqs).To(BeEmpty()) + }) + }) + When("there is a single matching resource", func() { + BeforeEach(func() { + withObjs = append(withObjs, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "configmap-1", + Labels: map[string]string{ + "vmoperator.vmware.com/vmi-cache": vmiCacheName, + }, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-2", + Name: "configmap-2", + }, + }, + ) + }) + Specify("one reconcile request should be returned", func() { + Expect(reqs).To(ConsistOf( + reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "ns-1", + Name: "configmap-1", + }, + }, + )) + }) + }) + When("there are multiple matching resources across multiple namespaces", func() { + BeforeEach(func() { + withObjs = append(withObjs, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-1", + Name: "configmap-1", + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-2", + Name: "configmap-2", + Labels: map[string]string{ + "vmoperator.vmware.com/vmi-cache": vmiCacheName, + }, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-3", + Name: "configmap-3", + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-4", + Name: "configmap-4", + Labels: map[string]string{ + "vmoperator.vmware.com/vmi-cache": vmiCacheName, + }, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-5", + Name: "configmap-5", + Labels: map[string]string{ + "vmoperator.vmware.com/vmi-cache": vmiCacheName, + }, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-6", + Name: "configmap-6", + }, + }, + ) + }) + Specify("an equal number of requests should be returned", func() { + Expect(reqs).To(ConsistOf( + reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "ns-2", + Name: "configmap-2", + }, + }, + reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "ns-4", + Name: "configmap-4", + }, + }, + reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "ns-5", + Name: "configmap-5", + }, + }, + )) + }) + }) + }) +}) diff --git a/pkg/util/vsphere/datastore/datastore.go b/pkg/util/vsphere/datastore/datastore.go new file mode 100644 index 000000000..7a2ce8b32 --- /dev/null +++ b/pkg/util/vsphere/datastore/datastore.go @@ -0,0 +1,138 @@ +// // © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package datastore + +import ( + "context" + "fmt" + "path" + "strings" + + "github.com/go-logr/logr" + "github.com/vmware/govmomi/fault" + "github.com/vmware/govmomi/object" + vimtypes "github.com/vmware/govmomi/vim25/types" + + pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" +) + +// CreateTopLevelDirectoryClient implements the client methods used by the +// CreateTopLevelDirectory method. +type CreateTopLevelDirectoryClient interface { + CreateDirectory( + ctx context.Context, + datastore *object.Datastore, + displayName, policy string) (string, error) + + ConvertNamespacePathToUuidPath( + ctx context.Context, + datacenter *object.Datacenter, + datastoreURL string) (string, error) + + MakeDirectory( + ctx context.Context, + name string, + datacenter *object.Datacenter, + createParentDirectories bool) error +} + +// CreateTopLevelDirectory creates a top-level directory at the root of the +// specified datastore. +func CreateTopLevelDirectory( + ctx context.Context, + client CreateTopLevelDirectoryClient, + datacenter *object.Datacenter, + datastore *object.Datastore, + datastoreName, datastoreURL string, + topLevelDirectoryCreateSupported bool, + directoryName string) (string, error) { + + if pkgutil.IsNil(ctx) { + panic("context is nil") + } + if pkgutil.IsNil(client) { + panic("client is nil") + } + if datacenter == nil { + panic("datacenter is nil") + } + if datastore == nil { + panic("datastore is nil") + } + if datastoreName == "" { + panic("datastoreName is empty") + } + if datastoreURL == "" { + panic("datastoreURL is empty") + } + if directoryName == "" { + panic("directoryName is empty") + } + + logger := logr.FromContextOrDiscard(ctx) + + if topLevelDirectoryCreateSupported { + logger.V(4).Info( + "Creating top-level directory with friendly-name", + "datastoreName", datastoreName, + "datastoreURL", datastoreURL, + "directoryName", directoryName) + p := fmt.Sprintf("[%s] %s", datastoreName, directoryName) + if err := client.MakeDirectory(ctx, p, datacenter, true); err != nil { + return "", fmt.Errorf( + "failed to create top-level directory %q: %w", p, err) + } + logger.V(4).Info("Created top-level directory", "path", p) + return p, nil + } + + // TODO(akutz) Figure out a way to test if the directory already exists + // instead of trying to just create it again and using the + // FileAlreadyExists error as signal. + datastorePath, _ := strings.CutPrefix(datastoreURL, "ds://") + directoryPath := path.Join(datastorePath, directoryName) + + logger.V(4).Info( + "Creating top-level directory with UUID", + "datastoreName", datastoreName, + "datastoreURL", datastoreURL, + "datastorePath", datastorePath, + "directoryPath", directoryPath) + + directoryPathUUID, err := client.CreateDirectory( + ctx, + datastore, + directoryPath, + "") + if err != nil { + if !fault.Is(err, &vimtypes.FileAlreadyExists{}) { + return "", fmt.Errorf("failed to create directory: %w", err) + } + + logger.V(4).Info( + "ConvertNamespacePathToUuidPath", + "datacenter", datacenter.Reference().Value, + "directoryPath", directoryPath) + + if directoryPathUUID, err = client.ConvertNamespacePathToUuidPath( + ctx, + datacenter, + directoryPath); err != nil { + + return "", fmt.Errorf( + "failed to convert namespace path=%q: %w", directoryPath, err) + } + } + + logger.V(4).Info( + "Got UUID version of top-level directory", + "directoryPathUUID", directoryPathUUID) + + directoryName = path.Base(directoryPathUUID) + directoryPath = fmt.Sprintf("[%s] %s", datastoreName, directoryName) + + logger.V(4).Info("Got top level directory", "directoryPath", directoryPath) + return directoryPath, nil +} diff --git a/pkg/util/vsphere/datastore/datastore_suite_test.go b/pkg/util/vsphere/datastore/datastore_suite_test.go new file mode 100644 index 000000000..cdb92dfb4 --- /dev/null +++ b/pkg/util/vsphere/datastore/datastore_suite_test.go @@ -0,0 +1,17 @@ +// © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package datastore_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestDatastore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Datastore Test Suite") +} diff --git a/pkg/util/vsphere/datastore/datastore_test.go b/pkg/util/vsphere/datastore/datastore_test.go new file mode 100644 index 000000000..809fbe27b --- /dev/null +++ b/pkg/util/vsphere/datastore/datastore_test.go @@ -0,0 +1,395 @@ +// © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package datastore_test + +import ( + "context" + "fmt" + "sync/atomic" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/soap" + vimtypes "github.com/vmware/govmomi/vim25/types" + + pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" + dsutil "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/datastore" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +type nilContextKey uint8 + +var nilContext = context.WithValue(context.Background(), nilContextKey(0), "nil") + +type fakeTopLevelDirectoryClient struct { + createErr error + createResult string + createCalls int32 + + convertErr error + convertResult string + convertCalls int32 + + makeErr error + makeCalls int32 + + vimClient *vim25.Client +} + +func (m *fakeTopLevelDirectoryClient) CreateDirectory( + ctx context.Context, + datastore *object.Datastore, + displayName, policy string) (string, error) { + + _ = atomic.AddInt32(&m.createCalls, 1) + if m.vimClient == nil { + return m.createResult, m.createErr + } + return object.NewDatastoreNamespaceManager(m.vimClient). + CreateDirectory(ctx, datastore, displayName, "") +} + +func (m *fakeTopLevelDirectoryClient) ConvertNamespacePathToUuidPath( //nolint:revive,stylecheck + ctx context.Context, + datacenter *object.Datacenter, + datastoreURL string) (string, error) { + + _ = atomic.AddInt32(&m.convertCalls, 1) + if m.vimClient == nil { + return m.convertResult, m.convertErr + } + return object.NewDatastoreNamespaceManager(m.vimClient). + ConvertNamespacePathToUuidPath(ctx, datacenter, datastoreURL) +} + +func (m *fakeTopLevelDirectoryClient) MakeDirectory( + ctx context.Context, + name string, + datacenter *object.Datacenter, + createParentDirectories bool) error { + + _ = atomic.AddInt32(&m.makeCalls, 1) + if m.vimClient == nil { + return m.makeErr + } + return object.NewFileManager(m.vimClient). + MakeDirectory(ctx, name, datacenter, createParentDirectories) +} + +var _ = Describe("CreateTopLevelDirectory", func() { + + var _ = DescribeTable("it should panic", + func( + ctx context.Context, + client dsutil.CreateTopLevelDirectoryClient, + datacenter *object.Datacenter, + datastore *object.Datastore, + datastoreName, + datastoreURL string, + topLevelDirectoryCreateSupported bool, + directoryName, + expPanic string) { + + if ctx == nilContext { + ctx = nil + } + + f := func() { + _, _ = dsutil.CreateTopLevelDirectory( + ctx, + client, + datacenter, + datastore, + datastoreName, + datastoreURL, + topLevelDirectoryCreateSupported, + directoryName) + } + + Expect(f).To(PanicWith(expPanic)) + }, + + Entry( + "nil ctx", + nilContext, + &fakeTopLevelDirectoryClient{}, + &object.Datacenter{}, + &object.Datastore{}, + "my-datastore", + "ds://my-datastore", + false, + "my-directory-name", + "context is nil", + ), + Entry( + "nil client", + context.Background(), + nil, + &object.Datacenter{}, + &object.Datastore{}, + "my-datastore", + "ds://my-datastore", + false, + "my-directory-name", + "client is nil", + ), + Entry( + "nil datacenter", + context.Background(), + &fakeTopLevelDirectoryClient{}, + nil, + &object.Datastore{}, + "my-datastore", + "ds://my-datastore", + false, + "my-directory-name", + "datacenter is nil", + ), + Entry( + "nil datastore", + context.Background(), + &fakeTopLevelDirectoryClient{}, + &object.Datacenter{}, + nil, + "my-datastore", + "ds://my-datastore", + false, + "my-directory-name", + "datastore is nil", + ), + Entry( + "empty datastoreName", + context.Background(), + &fakeTopLevelDirectoryClient{}, + &object.Datacenter{}, + &object.Datastore{}, + "", + "ds://my-datastore", + false, + "my-directory-name", + "datastoreName is empty", + ), + Entry( + "empty datastoreURL", + context.Background(), + &fakeTopLevelDirectoryClient{}, + &object.Datacenter{}, + &object.Datastore{}, + "my-datastore", + "", + false, + "my-directory-name", + "datastoreURL is empty", + ), + Entry( + "empty directoryName", + context.Background(), + &fakeTopLevelDirectoryClient{}, + &object.Datacenter{}, + &object.Datastore{}, + "my-datastore", + "ds://my-datastore", + false, + "", + "directoryName is empty", + ), + ) + + var _ = When("it should not panic", func() { + + const datastoreUUID = "1052936d-d3d9-44c4-a604-612ca9643b63" + + var ( + ctx context.Context + client *fakeTopLevelDirectoryClient + datacenter *object.Datacenter + datastore *object.Datastore + datastoreName string + datastoreURL string + topLevelDirectoryCreateSupported bool + directoryName string + + err error + out string + ) + + BeforeEach(func() { + ctx = context.Background() + client = &fakeTopLevelDirectoryClient{} + datacenter = object.NewDatacenter( + nil, vimtypes.ManagedObjectReference{ + Type: "Datacenter", + Value: "datacenter-1", + }) + datastore = object.NewDatastore( + nil, vimtypes.ManagedObjectReference{ + Type: "Datastore", + Value: "datastore-1", + }) + datastoreName = "my-datastore" + datastoreURL = "ds://my-datastore" + topLevelDirectoryCreateSupported = true + directoryName = "my-directory-name" + }) + + JustBeforeEach(func() { + out, err = dsutil.CreateTopLevelDirectory( + ctx, + client, + datacenter, + datastore, + datastoreName, + datastoreURL, + topLevelDirectoryCreateSupported, + directoryName) + }) + + When("datastore supports top-level directories", func() { + It("should return the expected path", func() { + Expect(client.makeCalls).To(Equal(int32(1))) + Expect(client.createCalls).To(BeZero()) + Expect(client.convertCalls).To(BeZero()) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(Equal("[" + datastoreName + "] " + directoryName)) + }) + When("make directory returns an error", func() { + BeforeEach(func() { + client.makeErr = fmt.Errorf( + "nested %w", + soap.WrapVimFault(&vimtypes.RuntimeFault{})) + }) + It("should return the error", func() { + Expect(client.makeCalls).To(Equal(int32(1))) + Expect(client.createCalls).To(BeZero()) + Expect(client.convertCalls).To(BeZero()) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(soap.WrapVimFault( + &vimtypes.RuntimeFault{}))) + }) + }) + }) + + When("datastore does not support top-level directories", func() { + BeforeEach(func() { + topLevelDirectoryCreateSupported = false + client.createResult = "ds://vmfs/volumes/" + datastoreUUID + "/" + directoryName + }) + It("should return the expected path", func() { + Expect(client.makeCalls).To(BeZero()) + Expect(client.createCalls).To(Equal(int32(1))) + Expect(client.convertCalls).To(BeZero()) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(Equal("[" + datastoreName + "] " + directoryName)) + }) + + When("create directory returns an error", func() { + When("error is not FileAlreadyExists", func() { + BeforeEach(func() { + client.createErr = fmt.Errorf( + "nested %w", + soap.WrapVimFault(&vimtypes.RuntimeFault{})) + }) + + It("should return the error", func() { + Expect(client.makeCalls).To(BeZero()) + Expect(client.createCalls).To(Equal(int32(1))) + Expect(client.convertCalls).To(BeZero()) + + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(soap.WrapVimFault( + &vimtypes.RuntimeFault{}))) + }) + }) + When("error is FileAlreadyExists", func() { + BeforeEach(func() { + client.createErr = fmt.Errorf( + "nested %w", + soap.WrapVimFault(&vimtypes.FileAlreadyExists{})) + + client.convertResult = directoryName + }) + + It("should return the expected path", func() { + Expect(client.makeCalls).To(BeZero()) + Expect(client.createCalls).To(Equal(int32(1))) + Expect(client.convertCalls).To(Equal(int32(1))) + + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(Equal("[" + datastoreName + "] " + directoryName)) + }) + + When("convert path returns an error", func() { + BeforeEach(func() { + client.convertErr = fmt.Errorf( + "nested %w", + soap.WrapVimFault(&vimtypes.RuntimeFault{})) + }) + + It("should return the error", func() { + Expect(client.makeCalls).To(BeZero()) + Expect(client.createCalls).To(Equal(int32(1))) + Expect(client.convertCalls).To(Equal(int32(1))) + + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(soap.WrapVimFault( + &vimtypes.RuntimeFault{}))) + }) + }) + }) + }) + }) + + Context("vcSim", Ordered, func() { + var ( + vcSimCtx *builder.TestContextForVCSim + simDatastore *simulator.Datastore + ) + + BeforeAll(func() { + ctx = pkgcfg.WithContext(ctx, pkgcfg.Config{}) + vcSimCtx = builder.NewTestContextForVCSim(ctx, builder.VCSimTestConfig{}) + client.vimClient = vcSimCtx.VCClient.Client + simDatastore = simulator.Map.Any("Datastore").(*simulator.Datastore) + datastore = object.NewDatastore(client.vimClient, simDatastore.Reference()) + datastoreName = simDatastore.Name + }) + + When("datastore supports top-level directories", func() { + BeforeEach(func() { + topLevelDirectoryCreateSupported = true + simDatastore.Capability.TopLevelDirectoryCreateSupported = ptr.To(true) + }) + It("should return the expected path", func() { + Expect(client.makeCalls).To(Equal(int32(1))) + Expect(client.createCalls).To(BeZero()) + Expect(client.convertCalls).To(BeZero()) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(Equal("[" + datastoreName + "] " + directoryName)) + }) + }) + + // TODO(akutz) vC Sim does not implement the CreateDirectory API + // at the moment. + XWhen("datastore does not support top-level directories", func() { + BeforeEach(func() { + topLevelDirectoryCreateSupported = false + simDatastore.Capability.TopLevelDirectoryCreateSupported = ptr.To(false) + }) + It("should return the expected path", func() { + Expect(client.makeCalls).To(BeZero()) + Expect(client.createCalls).To(Equal(int32(1))) + Expect(client.convertCalls).To(BeZero()) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(Equal("[" + datastoreName + "] " + directoryName)) + }) + }) + }) + }) +}) diff --git a/pkg/util/vsphere/library/item_cache.go b/pkg/util/vsphere/library/item_cache.go index 18bea8c67..6a90f0588 100644 --- a/pkg/util/vsphere/library/item_cache.go +++ b/pkg/util/vsphere/library/item_cache.go @@ -12,7 +12,6 @@ import ( "path" "strings" - "github.com/go-logr/logr" "github.com/vmware/govmomi/fault" "github.com/vmware/govmomi/object" vimtypes "github.com/vmware/govmomi/vim25/types" @@ -112,7 +111,7 @@ func copyDisk( return "", fmt.Errorf("failed to query disk uuid: %w", queryDiskErr) } - // Create the VM folder. + // Ensure the directory where the disks will be cached exists. if err := client.MakeDirectory( ctx, dstDir, @@ -146,112 +145,9 @@ func copyDisk( return dstFilePath, nil } -const topLevelCacheDirName = ".contentlib-cache" - -// GetTopLevelCacheDirClient implements the client methods used by the -// GetTopLevelCacheDir method. -type GetTopLevelCacheDirClient interface { - CreateDirectory( - ctx context.Context, - datastore *object.Datastore, - displayName, policy string) (string, error) - - ConvertNamespacePathToUuidPath( - ctx context.Context, - datacenter *object.Datacenter, - datastoreURL string) (string, error) -} - -// GetTopLevelCacheDir returns the top-level cache directory at the root of the -// datastore. -// If the datastore uses vSAN, this function also ensures the top-level -// directory exists. -func GetTopLevelCacheDir( - ctx context.Context, - client GetTopLevelCacheDirClient, - dstDatacenter *object.Datacenter, - dstDatastore *object.Datastore, - dstDatastoreName, dstDatastoreURL string, - topLevelDirectoryCreateSupported bool) (string, error) { - - if pkgutil.IsNil(ctx) { - panic("context is nil") - } - if pkgutil.IsNil(client) { - panic("client is nil") - } - if dstDatacenter == nil { - panic("dstDatacenter is nil") - } - if dstDatastore == nil { - panic("dstDatastore is nil") - } - if dstDatastoreName == "" { - panic("dstDatastoreName is empty") - } - if dstDatastoreURL == "" { - panic("dstDatastoreURL is empty") - } - - logger := logr.FromContextOrDiscard(ctx).WithName("GetTopLevelCacheDir") - - logger.V(4).Info( - "Args", - "dstDatastoreName", dstDatastoreName, - "dstDatastoreURL", dstDatastoreURL, - "topLevelDirectoryCreateSupported", topLevelDirectoryCreateSupported) - - if topLevelDirectoryCreateSupported { - return fmt.Sprintf( - "[%s] %s", dstDatastoreName, topLevelCacheDirName), nil - } - - // TODO(akutz) Figure out a way to test if the directory already exists - // instead of trying to just create it again and using the - // FileAlreadyExists error as signal. - - dstDatastorePath, _ := strings.CutPrefix(dstDatastoreURL, "ds://") - topLevelCacheDirPath := path.Join(dstDatastorePath, topLevelCacheDirName) - - logger.V(4).Info( - "CreateDirectory", - "dstDatastorePath", dstDatastorePath, - "topLevelCacheDirPath", topLevelCacheDirPath) - - uuidTopLevelCacheDirPath, err := client.CreateDirectory( - ctx, - dstDatastore, - topLevelCacheDirPath, - "") - if err != nil { - if !fault.Is(err, &vimtypes.FileAlreadyExists{}) { - return "", fmt.Errorf("failed to create directory: %w", err) - } - - logger.V(4).Info( - "ConvertNamespacePathToUuidPath", - "dstDatacenter", dstDatacenter.Reference().Value, - "topLevelCacheDirPath", topLevelCacheDirPath) - - uuidTopLevelCacheDirPath, err = client.ConvertNamespacePathToUuidPath( - ctx, - dstDatacenter, - topLevelCacheDirPath) - if err != nil { - return "", fmt.Errorf( - "failed to convert namespace path=%q: %w", - topLevelCacheDirPath, err) - } - } - - logger.V(4).Info( - "Got absolute top level cache dir path", - "uuidTopLevelCacheDirPath", uuidTopLevelCacheDirPath) - - topLevelCacheDirName := path.Base(uuidTopLevelCacheDirPath) - - return fmt.Sprintf("[%s] %s", dstDatastoreName, topLevelCacheDirName), nil -} +// TopLevelCacheDirName is the name of the top-level cache directory created on +// each datastore. +const TopLevelCacheDirName = ".contentlib-cache" // GetCacheDirForLibraryItem returns the cache directory for a library item // beneath a top-level cache directory. @@ -267,6 +163,11 @@ func GetCacheDirForLibraryItem( if contentVersion == "" { panic("contentVersion is empty") } + + // Encode the contentVersion to ensure it is safe to use as part of the + // directory name. + contentVersion = sha1sum17(contentVersion) + return path.Join(topLevelCacheDir, itemUUID, contentVersion) } @@ -279,7 +180,11 @@ func GetCachedFileNameForVMDK(vmdkFileName string) string { if ext := path.Ext(vmdkFileName); ext != "" { vmdkFileName, _ = strings.CutSuffix(vmdkFileName, ext) } + return sha1sum17(vmdkFileName) +} + +func sha1sum17(s string) string { h := sha1.New() //nolint:gosec // used for creating directory name - _, _ = io.WriteString(h, vmdkFileName) + _, _ = io.WriteString(h, s) return fmt.Sprintf("%x", h.Sum(nil))[0:17] } diff --git a/pkg/util/vsphere/library/item_cache_test.go b/pkg/util/vsphere/library/item_cache_test.go index 0e3ecc19d..1e5a6c22d 100644 --- a/pkg/util/vsphere/library/item_cache_test.go +++ b/pkg/util/vsphere/library/item_cache_test.go @@ -6,7 +6,6 @@ package library_test import ( "context" - "fmt" "sync/atomic" . "github.com/onsi/ginkgo/v2" @@ -318,254 +317,6 @@ var _ = Describe("CacheStorageURIs", func() { }) }) -type fakeGetTopLevelCacheDirClient struct { - createErr error - createResult string - createCalls int32 - - convertErr error - convertResult string - convertCalls int32 -} - -func (m *fakeGetTopLevelCacheDirClient) CreateDirectory( - ctx context.Context, - datastore *object.Datastore, - displayName, policy string) (string, error) { - - _ = atomic.AddInt32(&m.createCalls, 1) - return m.createResult, m.createErr -} - -func (m *fakeGetTopLevelCacheDirClient) ConvertNamespacePathToUuidPath( //nolint:revive,stylecheck - ctx context.Context, - datacenter *object.Datacenter, - datastoreURL string) (string, error) { - - _ = atomic.AddInt32(&m.convertCalls, 1) - return m.convertResult, m.convertErr -} - -var _ = Describe("GetTopLevelCacheDir", func() { - - var _ = DescribeTable("it should panic", - func( - ctx context.Context, - client clsutil.GetTopLevelCacheDirClient, - dstDatacenter *object.Datacenter, - dstDatastore *object.Datastore, - dstDatastoreName, - dstDatastoreURL string, - topLevelDirectoryCreateSupported bool, - expPanic string) { - - if ctx == nilContext { - ctx = nil - } - - f := func() { - _, _ = clsutil.GetTopLevelCacheDir( - ctx, - client, - dstDatacenter, - dstDatastore, - dstDatastoreName, - dstDatastoreURL, - topLevelDirectoryCreateSupported) - } - - Expect(f).To(PanicWith(expPanic)) - }, - - Entry( - "nil ctx", - nilContext, - &fakeGetTopLevelCacheDirClient{}, - &object.Datacenter{}, - &object.Datastore{}, - "my-datastore", - "ds://my-datastore", - false, - "context is nil", - ), - Entry( - "nil client", - context.Background(), - nil, - &object.Datacenter{}, - &object.Datastore{}, - "my-datastore", - "ds://my-datastore", - false, - "client is nil", - ), - Entry( - "nil dstDatacenter", - context.Background(), - &fakeGetTopLevelCacheDirClient{}, - nil, - &object.Datastore{}, - "my-datastore", - "ds://my-datastore", - false, - "dstDatacenter is nil", - ), - Entry( - "nil dstDatastore", - context.Background(), - &fakeGetTopLevelCacheDirClient{}, - &object.Datacenter{}, - nil, - "my-datastore", - "ds://my-datastore", - false, - "dstDatastore is nil", - ), - Entry( - "empty dstDatastoreName", - context.Background(), - &fakeGetTopLevelCacheDirClient{}, - &object.Datacenter{}, - &object.Datastore{}, - "", - "ds://my-datastore", - false, - "dstDatastoreName is empty", - ), - Entry( - "empty dstDatastoreURL", - context.Background(), - &fakeGetTopLevelCacheDirClient{}, - &object.Datacenter{}, - &object.Datastore{}, - "my-datastore", - "", - false, - "dstDatastoreURL is empty", - ), - ) - - var _ = When("it should not panic", func() { - - var ( - ctx context.Context - client *fakeGetTopLevelCacheDirClient - dstDatacenter *object.Datacenter - dstDatastore *object.Datastore - dstDatastoreName string - dstDatastoreURL string - topLevelDirectoryCreateSupported bool - - err error - out string - ) - - BeforeEach(func() { - ctx = context.Background() - client = &fakeGetTopLevelCacheDirClient{} - dstDatacenter = object.NewDatacenter( - nil, vimtypes.ManagedObjectReference{ - Type: "Datacenter", - Value: "datacenter-1", - }) - dstDatastore = object.NewDatastore( - nil, vimtypes.ManagedObjectReference{ - Type: "Datastore", - Value: "datastore-1", - }) - dstDatastoreName = "my-datastore" - dstDatastoreURL = "ds://my-datastore" - topLevelDirectoryCreateSupported = true - }) - - JustBeforeEach(func() { - out, err = clsutil.GetTopLevelCacheDir( - ctx, - client, - dstDatacenter, - dstDatastore, - dstDatastoreName, - dstDatastoreURL, - topLevelDirectoryCreateSupported) - }) - - When("datastore supports top-level directories", func() { - It("should return the expected path", func() { - Expect(err).ToNot(HaveOccurred()) - Expect(out).To(Equal("[my-datastore] .contentlib-cache")) - }) - }) - - When("datastore does not support top-level directories", func() { - BeforeEach(func() { - topLevelDirectoryCreateSupported = false - client.createResult = "ds://vmfs/volumes/123/abc" - }) - It("should return the expected path", func() { - Expect(client.createCalls).To(Equal(int32(1))) - Expect(client.convertCalls).To(BeZero()) - - Expect(err).ToNot(HaveOccurred()) - Expect(out).To(Equal("[my-datastore] abc")) - }) - - When("create directory returns an error", func() { - When("error is not FileAlreadyExists", func() { - BeforeEach(func() { - client.createErr = fmt.Errorf( - "nested %w", - soap.WrapVimFault(&vimtypes.RuntimeFault{})) - }) - - It("should return the error", func() { - Expect(client.createCalls).To(Equal(int32(1))) - Expect(client.convertCalls).To(BeZero()) - - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(soap.WrapVimFault( - &vimtypes.RuntimeFault{}))) - }) - }) - When("error is FileAlreadyExists", func() { - BeforeEach(func() { - client.createErr = fmt.Errorf( - "nested %w", - soap.WrapVimFault(&vimtypes.FileAlreadyExists{})) - - client.convertResult = "abc" - }) - - It("should return the expected path", func() { - Expect(client.createCalls).To(Equal(int32(1))) - Expect(client.convertCalls).To(Equal(int32(1))) - - Expect(err).ToNot(HaveOccurred()) - Expect(out).To(Equal("[my-datastore] abc")) - }) - - When("convert path returns an error", func() { - BeforeEach(func() { - client.convertErr = fmt.Errorf( - "nested %w", - soap.WrapVimFault(&vimtypes.RuntimeFault{})) - }) - - It("should return the error", func() { - Expect(client.createCalls).To(Equal(int32(1))) - Expect(client.convertCalls).To(Equal(int32(1))) - - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(soap.WrapVimFault( - &vimtypes.RuntimeFault{}))) - }) - }) - }) - }) - - }) - }) -}) - var _ = DescribeTable("GetCacheDirForLibraryItem", func(topLevelCacheDir, itemUUID, contentVersion, expOut, expPanic string) { var out string @@ -603,13 +354,13 @@ var _ = DescribeTable("GetCacheDirForLibraryItem", Entry( "absolute topLevelCacheDir", "/a", "b", "c", - "/a/b/c", + "/a/b/84a516841ba77a5b4", "", ), Entry( "relative topLevelCacheDir", "a", "b", "c", - "a/b/c", + "a/b/84a516841ba77a5b4", "", ), ) diff --git a/pkg/util/vsphere/library/item_sync.go b/pkg/util/vsphere/library/item_sync.go deleted file mode 100644 index 7b77b4a93..000000000 --- a/pkg/util/vsphere/library/item_sync.go +++ /dev/null @@ -1,58 +0,0 @@ -// © Broadcom. All Rights Reserved. -// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. -// SPDX-License-Identifier: Apache-2.0 - -package library - -import ( - "context" - "fmt" - - "github.com/go-logr/logr" - "github.com/vmware/govmomi/vapi/library" - - pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" -) - -// SyncLibraryItemClient implements the client methods used by the -// SyncLibraryItem method. -type SyncLibraryItemClient interface { - GetLibraryItem(ctx context.Context, id string) (*library.Item, error) - SyncLibraryItem(ctx context.Context, item *library.Item, force bool) error -} - -// SyncLibraryItem issues a sync call to the provided library item. -func SyncLibraryItem( - ctx context.Context, - client SyncLibraryItemClient, - itemID string) error { - - if pkgutil.IsNil(ctx) { - panic("context is nil") - } - if pkgutil.IsNil(client) { - panic("client is nil") - } - if itemID == "" { - panic("itemID is empty") - } - - logger := logr.FromContextOrDiscard(ctx) - - // A file from a library item that belongs to a subscribed library may not - // be fully available. Sync the file to ensure it is present. - logger.Info("Syncing content library item", "libraryItemID", itemID) - libItem, err := client.GetLibraryItem(ctx, itemID) - if err != nil { - return fmt.Errorf( - "error getting library item %s: %w", itemID, err) - } - if libItem.Type == "LOCAL" { - return nil - } - if err := client.SyncLibraryItem(ctx, libItem, true); err != nil { - return fmt.Errorf( - "error syncing library item %s: %w", itemID, err) - } - return nil -} diff --git a/pkg/util/vsphere/library/item_sync_test.go b/pkg/util/vsphere/library/item_sync_test.go deleted file mode 100644 index 0a07aff2d..000000000 --- a/pkg/util/vsphere/library/item_sync_test.go +++ /dev/null @@ -1,171 +0,0 @@ -// © Broadcom. All Rights Reserved. -// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. -// SPDX-License-Identifier: Apache-2.0 - -package library_test - -import ( - "context" - "errors" - "fmt" - "sync/atomic" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/go-logr/logr" - "github.com/vmware/govmomi/vapi/library" - - clsutil "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/library" -) - -type fakeSyncLibraryItemClient struct { - getLibItemItem *library.Item - getLibItemErr error - getLibItemCalls int32 - syncLibItemErr error - syncLibItemCalls int32 -} - -func (m *fakeSyncLibraryItemClient) GetLibraryItem( - ctx context.Context, - id string) (*library.Item, error) { - - _ = atomic.AddInt32(&m.getLibItemCalls, 1) - return m.getLibItemItem, m.getLibItemErr -} - -func (m *fakeSyncLibraryItemClient) SyncLibraryItem( - ctx context.Context, - item *library.Item, force bool) error { - - _ = atomic.AddInt32(&m.syncLibItemCalls, 1) - return m.syncLibItemErr -} - -var _ = Describe("SyncLibraryItem", func() { - - var ( - ctx context.Context - client *fakeSyncLibraryItemClient - itemID string - ) - - BeforeEach(func() { - ctx = context.Background() - client = &fakeSyncLibraryItemClient{} - itemID = "fake" - }) - - When("it should panic", func() { - When("context is nil", func() { - BeforeEach(func() { - ctx = nil - }) - It("should panic", func() { - Expect(func() { - _ = clsutil.SyncLibraryItem(ctx, client, itemID) - }).To(PanicWith("context is nil")) - }) - }) - When("client is nil", func() { - BeforeEach(func() { - client = nil - }) - It("should panic", func() { - client = nil - Expect(func() { - _ = clsutil.SyncLibraryItem(ctx, client, itemID) - }).To(PanicWith("client is nil")) - }) - }) - When("library item ID is empty", func() { - BeforeEach(func() { - itemID = "" - }) - It("should panic", func() { - Expect(func() { - _ = clsutil.SyncLibraryItem(ctx, client, itemID) - }).To(PanicWith("itemID is empty")) - }) - }) - }) - - When("it should not panic", func() { - - var ( - syncErr error - ) - - BeforeEach(func() { - ctx = logr.NewContext(context.Background(), GinkgoLogr) - }) - - JustBeforeEach(func() { - syncErr = clsutil.SyncLibraryItem(ctx, client, itemID) - }) - - When("item does not exist", func() { - BeforeEach(func() { - client.getLibItemErr = errors.New("404 Not Found") - }) - It("should 404", func() { - Expect(syncErr).To(HaveOccurred()) - Expect(syncErr).To(MatchError(fmt.Errorf( - "error getting library item %s: %w", itemID, - client.getLibItemErr))) - Expect(atomic.LoadInt32(&client.getLibItemCalls)).To(Equal(int32(1))) - - }) - }) - - When("item does exist", func() { - BeforeEach(func() { - client.getLibItemItem = &library.Item{ - ID: itemID, - Type: "LOCAL", - } - }) - When("the item is local", func() { - BeforeEach(func() { - client.getLibItemItem.Type = "LOCAL" - }) - It("should succeed without calling sync", func() { - Expect(syncErr).ToNot(HaveOccurred()) - Expect(atomic.LoadInt32(&client.getLibItemCalls)).To(Equal(int32(1))) - Expect(atomic.LoadInt32(&client.syncLibItemCalls)).To(Equal(int32(0))) - }) - }) - When("the item is subscribed", func() { - BeforeEach(func() { - client.getLibItemItem.Type = "SUBSCRIBED" - }) - When("there is an issue with syncing", func() { - BeforeEach(func() { - client.syncLibItemErr = errors.New("timed out") - }) - It("should return an error", func() { - Expect(syncErr).To(HaveOccurred()) - Expect(syncErr).To(MatchError(fmt.Errorf( - "error syncing library item %s: %w", itemID, - client.syncLibItemErr))) - Expect(atomic.LoadInt32(&client.getLibItemCalls)).To(Equal(int32(1))) - Expect(atomic.LoadInt32(&client.syncLibItemCalls)).To(Equal(int32(1))) - }) - }) - When("there is no issue with syncing", func() { - BeforeEach(func() { - client.getLibItemItem = &library.Item{ - ID: itemID, - } - }) - It("should succeed", func() { - Expect(syncErr).ToNot(HaveOccurred()) - Expect(atomic.LoadInt32(&client.getLibItemCalls)).To(Equal(int32(1))) - Expect(atomic.LoadInt32(&client.syncLibItemCalls)).To(Equal(int32(1))) - }) - }) - }) - }) - }) -}) diff --git a/test/builder/fake.go b/test/builder/fake.go index d6654821f..55fa92caf 100644 --- a/test/builder/fake.go +++ b/test/builder/fake.go @@ -60,6 +60,7 @@ func KnownObjectTypes() []client.Object { &vmopv1.VirtualMachinePublishRequest{}, &vmopv1.ClusterVirtualMachineImage{}, &vmopv1.VirtualMachineImage{}, + &vmopv1.VirtualMachineImageCache{}, &vmopv1.VirtualMachineWebConsoleRequest{}, &vmopv1a1.WebConsoleRequest{}, &cnsv1alpha1.CnsNodeVmAttachment{}, diff --git a/test/builder/testdata/images/ttylinux-pc_i486-16.1.mf b/test/builder/testdata/images/ttylinux-pc_i486-16.1.mf index fd935c37f..bcfe83915 100644 --- a/test/builder/testdata/images/ttylinux-pc_i486-16.1.mf +++ b/test/builder/testdata/images/ttylinux-pc_i486-16.1.mf @@ -1,2 +1,2 @@ -SHA1(ttylinux-pc_i486-16.1-disk1.vmdk)= ed64564a37366bfe1c93af80e2ead0cbd398c3d3 - \ No newline at end of file +SHA1 (ttylinux-pc_i486-16.1.ovf) = 87271da9a5ae979f29e0ce7be824df428c178914 +SHA1 (ttylinux-pc_i486-16.1-disk1.vmdk) = ed64564a37366bfe1c93af80e2ead0cbd398c3d3 diff --git a/test/builder/testdata/images/ttylinux-pc_i486-16.1.ova b/test/builder/testdata/images/ttylinux-pc_i486-16.1.ova index 5d878c74fb714b6138e957175e999605df92a1f9..30f078bd7b7042f60897bd0ba4931c90128161b9 100644 GIT binary patch delta 938 zcmbV|Npn;M6h@&TqA>~~h#?&1B?=_s@TTt9jRs^!WKvXAP<$la7$FWJgCj(SgN!EK zDyS&1v0S*qg@4b7EL`b=TXpNJ@7BFvox0Q4&$Ue#J#(Z|Nz-6X7=~FT`$xn0t}jMW zREg3cO0%RACWTLfFe;amcuo+uW>*Y%OdKE2$BNth=*0Nd&OdI^Rty@A+1}*~i|wNB z1{PYZ|9-o)v9r4|GB!GukJbBv$sw zjFaqNtT?%Ym8AEG)$E>M3i@Z3B?G}=P|e~j8qRBZIj`5US|hI2!=ZY1xL%FZ;YJ!) zhoY=ni_-Re+c!i(f3qhj4Fs)K-E00@9JJ?in?3&xUJjEuyYrJMPUCWx#%Z}yfLF34 zy|;g!Lgq?I=E;0%llx?W+%FGEyF4fz@{l|%3uTdX%3|q~CGv2q zd0bXVAidHjPso$9QdY@o>6Za{O4i8J@{Bwy&q*ke#F9uVWvNIeRjJ8Zd0t+SL0Kp3 zMEAp+($gDJ_CEv;S@`GHJYx1M~BtOe_`9*$} L8*=kEZr%PHSLX}5 delta 859 zcmb7==~B-D6oxA$dy-OlP|2F**MG~tWG74ZohaEVTI@^0lO?JC$P)6!1>_3cLAT-# z{66@i8HRV}%sKO(ne#q#+6Ou_+QaIpPXvirEF}n49t6Mj&?%3HUU?)IiC4rTv3Mff zhgK|_5+r+bYqQ!~T5Fn9ic(sd8tR%F>w1d|GJlQz-%C@6htevuzjfy{rzHn-tNQZ( z8WjYQXmn&$ED;aU{w1m}KRsEUUy^LiFN+lYx00>7UH!wQsXzJ({|p%qqLGU5O(P)_ zk!bwiAUlv$Nt02ME*UafGG&a6m2om&CdfpYBv~?9vSo_o$W+OdJeel>QXqvgUBa6) zWTwoLA}N*SV=$VIs%m!&}(rAe;HRcV$MX_Yp)CfDVL+>~2#TkgnRxhMCf zT^>k>Jd{WBSd>nAB2VR+JeMwcA>Hy)dZbtSBq^`twY-sjc`NVay?l@X`6!>{vkZRa G%g|5Ufc9|! diff --git a/test/builder/testdata/images/ttylinux-pc_i486-16.1.ovf b/test/builder/testdata/images/ttylinux-pc_i486-16.1.ovf index ccf48c497..73fe8ca70 100644 --- a/test/builder/testdata/images/ttylinux-pc_i486-16.1.ovf +++ b/test/builder/testdata/images/ttylinux-pc_i486-16.1.ovf @@ -91,4 +91,4 @@ - + \ No newline at end of file diff --git a/test/builder/vcsim_test_context.go b/test/builder/vcsim_test_context.go index 48580166d..cad0547f6 100644 --- a/test/builder/vcsim_test_context.go +++ b/test/builder/vcsim_test_context.go @@ -66,6 +66,7 @@ import ( pkgmgr "github.com/vmware-tanzu/vm-operator/pkg/manager" "github.com/vmware-tanzu/vm-operator/pkg/record" "github.com/vmware-tanzu/vm-operator/pkg/util/ovfcache" + "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" pkgclient "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/client" "github.com/vmware-tanzu/vm-operator/test/testutil" ) @@ -157,6 +158,7 @@ type TestContextForVCSim struct { withWorkloadIsolation bool // When WithContentLibrary is true: + LocalContentLibraryID string ContentLibraryImageName string ContentLibraryID string ContentLibraryItemID string @@ -620,8 +622,9 @@ func (c *TestContextForVCSim) setupContentLibrary(config VCSimTestConfig) { libMgr := library.NewManager(c.RestClient) - libSpec := library.Library{ - Name: "vmop-content-library", + // Create a local library. + localLibSpec := library.Library{ + Name: "vmop-local", Type: "LOCAL", Storage: []library.StorageBacking{ { @@ -629,64 +632,116 @@ func (c *TestContextForVCSim) setupContentLibrary(config VCSimTestConfig) { Type: "DATASTORE", }, }, - // Making it published to be able to verify SyncLibraryItem() API. + // Publish the library. Publication: &library.Publication{ - Published: vimtypes.NewBool(true), + Published: ptr.To(true), }, } - - clID, err := libMgr.CreateLibrary(c, libSpec) + localLibID, err := libMgr.CreateLibrary(c, localLibSpec) Expect(err).ToNot(HaveOccurred()) - Expect(clID).ToNot(BeEmpty()) - c.ContentLibraryID = clID + Expect(localLibID).ToNot(BeEmpty()) + localLib, err := libMgr.GetLibraryByID(c, localLibID) + Expect(err).ToNot(HaveOccurred()) + Expect(localLib).ToNot(BeNil()) - // OVA - libraryItem := library.Item{ + c.LocalContentLibraryID = localLibID + + // Create an OVA in the local library. + localLibItemOVA := library.Item{ Name: "test-image-ovf", Type: library.ItemTypeOVF, - LibraryID: clID, + LibraryID: localLibID, } - c.ContentLibraryImageName = libraryItem.Name - - itemID := CreateContentLibraryItem( + localLibItemOVAID := CreateContentLibraryItem( c, libMgr, - libraryItem, + localLibItemOVA, path.Join( testutil.GetRootDirOrDie(), "test", "builder", "testdata", - "images", "ttylinux-pc_i486-16.1.ovf"), + "images", "ttylinux-pc_i486-16.1.ova"), ) - c.ContentLibraryItemID = itemID + Expect(localLibItemOVAID).ToNot(BeEmpty()) - // The image isn't quite as prod but sufficient for what we need here ATM. - clusterVMImage := DummyClusterVirtualMachineImage(c.ContentLibraryImageName) - clusterVMImage.Spec.ProviderRef = &common.LocalObjectRef{ - Kind: "ClusterContentLibraryItem", + { + li, err := libMgr.GetLibraryItem(c, localLibItemOVAID) + Expect(err).ToNot(HaveOccurred()) + Expect(li.Cached).To(BeTrue()) + Expect(li.ContentVersion).ToNot(BeEmpty()) } - Expect(c.Client.Create(c, clusterVMImage)).To(Succeed()) - clusterVMImage.Status.ProviderItemID = itemID - conditions.MarkTrue(clusterVMImage, vmopv1.ReadyConditionType) - Expect(c.Client.Status().Update(c, clusterVMImage)).To(Succeed()) - // ISO - libraryItem = library.Item{ + // Create an ISO in the local library. + localLibItemISO := library.Item{ Name: "test-image-iso", Type: library.ItemTypeISO, - LibraryID: clID, + LibraryID: localLibID, } - c.ContentLibraryIsoImageName = libraryItem.Name - - itemID = CreateContentLibraryItem( + localLibItemISOID := CreateContentLibraryItem( c, libMgr, - libraryItem, + localLibItemISO, path.Join( testutil.GetRootDirOrDie(), "test", "builder", "testdata", "images", "ttylinux-pc_i486-16.1.iso"), ) - c.ContentLibraryIsoItemID = itemID + Expect(localLibItemISOID).ToNot(BeEmpty()) + + // Create a subscribed library. + subLibSpec := library.Library{ + Name: "vmop-subscribed", + Type: "SUBSCRIBED", + Storage: []library.StorageBacking{ + { + DatastoreID: c.Datastore.Reference().Value, + Type: "DATASTORE", + }, + }, + Subscription: &library.Subscription{ + SubscriptionURL: localLib.Publication.PublishURL, + OnDemand: ptr.To(true), + }, + } + subLibID, err := libMgr.CreateLibrary(c, subLibSpec) + Expect(err).ToNot(HaveOccurred()) + Expect(subLibID).ToNot(BeEmpty()) + + // Get the library item IDs for the OVA & ISO from the subscribed library. + var ( + subLibItemOVAID string + subLibItemISOID string + ) + subLibItems, err := libMgr.GetLibraryItems(c, subLibID) + Expect(err).ToNot(HaveOccurred()) + for i := range subLibItems { + sli := subLibItems[i] + switch sli.Name { + case localLibItemOVA.Name: + subLibItemOVAID = sli.ID + case localLibItemISO.Name: + subLibItemISOID = sli.ID + } + } + Expect(subLibItemOVAID).ToNot(BeEmpty()) + Expect(subLibItemISOID).ToNot(BeEmpty()) + + c.ContentLibraryID = subLibID + + c.ContentLibraryImageName = localLibItemOVA.Name + c.ContentLibraryItemID = subLibItemOVAID + + c.ContentLibraryIsoImageName = localLibItemISO.Name + c.ContentLibraryIsoItemID = subLibItemISOID + + // The image isn't quite as prod but sufficient for what we need here ATM. + clusterVMImage := DummyClusterVirtualMachineImage(c.ContentLibraryImageName) + clusterVMImage.Spec.ProviderRef = &common.LocalObjectRef{ + Kind: "ClusterContentLibraryItem", + } + Expect(c.Client.Create(c, clusterVMImage)).To(Succeed()) + clusterVMImage.Status.ProviderItemID = subLibItemOVAID + conditions.MarkTrue(clusterVMImage, vmopv1.ReadyConditionType) + Expect(c.Client.Status().Update(c, clusterVMImage)).To(Succeed()) // The image isn't quite as prod but sufficient for what we need here ATM. clusterVMImage = DummyClusterVirtualMachineImage(c.ContentLibraryIsoImageName) @@ -694,7 +749,7 @@ func (c *TestContextForVCSim) setupContentLibrary(config VCSimTestConfig) { Kind: "ClusterContentLibraryItem", } Expect(c.Client.Create(c, clusterVMImage)).To(Succeed()) - clusterVMImage.Status.ProviderItemID = itemID + clusterVMImage.Status.ProviderItemID = subLibItemISOID conditions.MarkTrue(clusterVMImage, vmopv1.ReadyConditionType) Expect(c.Client.Status().Update(c, clusterVMImage)).To(Succeed()) }