From 6f5acbf1b3650540b08e67dfaadb8b7b62b7378b Mon Sep 17 00:00:00 2001 From: akutz Date: Wed, 13 Nov 2024 17:02:10 -0600 Subject: [PATCH] Deprecate ProbeManager for non-TCP probes This patch deprecates the use of ProbeManager for non-TCP probes when the AsyncSignal feature is enabled. This is because VMs will be reconciled as soon as their heartbeat or guestinfo information is updated, and thus readiness can be determined in the normal VM reconcile loop. --- .../virtualmachine_controller.go | 14 +- .../vsphere/vmlifecycle/update_status.go | 168 +++ .../vsphere/vmlifecycle/update_status_test.go | 1194 ++++++++++------- pkg/record/recorder_context.go | 37 + pkg/record/recorder_context_test.go | 57 + pkg/util/vsphere/watcher/watcher.go | 1 + test/builder/vcsim_test_context.go | 3 + 7 files changed, 986 insertions(+), 488 deletions(-) create mode 100644 pkg/record/recorder_context.go create mode 100644 pkg/record/recorder_context_test.go diff --git a/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go b/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go index 80965b00e..157e7efa3 100644 --- a/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go +++ b/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go @@ -280,6 +280,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re ctx = ctxop.WithContext(ctx) + ctx = record.WithContext(ctx, r.Recorder) + vm := &vmopv1.VirtualMachine{} if err := r.Get(ctx, req.NamespacedName, vm); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -451,9 +453,15 @@ func (r *Reconciler) ReconcileNormal(ctx *pkgctx.VirtualMachineContext) (reterr r.Recorder.EmitEvent(ctx.VM, "ReconcileNormal", err, true) } - // Always check if this VM needs to be in the probe queue. We want to do this even if - // the VM hasn't been created yet so the condition is updated. - r.Prober.AddToProberManager(ctx.VM) + if !pkgcfg.FromContext(ctx).Features.WorkloadDomainIsolation || + pkgcfg.FromContext(ctx).AsyncSignalDisabled { + + // Add the VM to the probe manager. This is idempotent. + r.Prober.AddToProberManager(ctx.VM) + } else if p := ctx.VM.Spec.ReadinessProbe; p != nil && p.TCPSocket != nil { + // TCP probes still use the probe manager. + r.Prober.AddToProberManager(ctx.VM) + } if err != nil { return err diff --git a/pkg/providers/vsphere/vmlifecycle/update_status.go b/pkg/providers/vsphere/vmlifecycle/update_status.go index e930b0720..5fb888eb2 100644 --- a/pkg/providers/vsphere/vmlifecycle/update_status.go +++ b/pkg/providers/vsphere/vmlifecycle/update_status.go @@ -9,9 +9,11 @@ import ( "net" "path" "reflect" + "regexp" "slices" "strings" + "github.com/go-logr/logr" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/vim25/mo" vimtypes "github.com/vmware/govmomi/vim25/types" @@ -29,6 +31,7 @@ import ( pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/network" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/vcenter" + vmoprecord "github.com/vmware-tanzu/vm-operator/pkg/record" "github.com/vmware-tanzu/vm-operator/pkg/topology" "github.com/vmware-tanzu/vm-operator/pkg/util" "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" @@ -95,6 +98,12 @@ func UpdateStatus( updateGuestNetworkStatus(vmCtx.VM, vmCtx.MoVM.Guest) updateStorageStatus(vmCtx.VM, vmCtx.MoVM) + if pkgcfg.FromContext(vmCtx).Features.WorkloadDomainIsolation && + !pkgcfg.FromContext(vmCtx).AsyncSignalDisabled { + + updateProbeStatus(vmCtx, vm, vmCtx.MoVM) + } + vm.Status.Host, err = getRuntimeHostHostname(vmCtx, vcVM, summary.Runtime.Host) if err != nil { errs = append(errs, err) @@ -880,3 +889,162 @@ const byteToGiB = 1 /* B */ * 1024 /* KiB */ * 1024 /* MiB */ * 1024 /* GiB */ func BytesToResourceGiB(b int64) *resource.Quantity { return ptr.To(resource.MustParse(fmt.Sprintf("%dGi", b/byteToGiB))) } + +type probeResult uint8 + +const ( + probeResultUnknown probeResult = iota + probeResultSuccess + probeResultFailure +) + +const ( + probeReasonReady string = "Ready" + probeReasonNotReady string = "NotReady" + probeReasonUnknown string = "Unknown" +) + +func heartbeatValue(value string) int { + switch value { + case string(vmopv1.GreenHeartbeatStatus): + return 3 + case string(vmopv1.YellowHeartbeatStatus): + return 2 + case string(vmopv1.RedHeartbeatStatus): + return 1 + case string(vmopv1.GrayHeartbeatStatus): + return 0 + default: + return -1 + } +} + +// updateProbeStatus updates a VM's status with the results of the configured +// readiness probes. +// Please note, this function returns early if there are no non-TCP readiness +// probes. +func updateProbeStatus( + ctx context.Context, + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine) { + + p := vm.Spec.ReadinessProbe + if p == nil || p.TCPSocket != nil { + return + } + + var ( + result probeResult + resultMsg string + ) + + switch { + case p.GuestHeartbeat != nil: + result, resultMsg = updateProbeStatusHeartbeat(vm, moVM) + case p.GuestInfo != nil: + result, resultMsg = updateProbeStatusGuestInfo(vm, moVM) + } + + var cond *metav1.Condition + switch result { + case probeResultSuccess: + cond = conditions.TrueCondition(vmopv1.ReadyConditionType) + case probeResultFailure: + cond = conditions.FalseCondition( + vmopv1.ReadyConditionType, probeReasonNotReady, resultMsg) + default: + cond = conditions.UnknownCondition( + vmopv1.ReadyConditionType, probeReasonUnknown, resultMsg) + } + + // Emit event whe the condition is added or its status changes. + if c := conditions.Get(vm, cond.Type); c == nil || c.Status != cond.Status { + recorder := vmoprecord.FromContext(ctx) + if cond.Status == metav1.ConditionTrue { + recorder.Eventf(vm, probeReasonReady, "") + } else { + recorder.Eventf(vm, cond.Reason, cond.Message) + } + + // Log the time when the VM changes its readiness condition. + logr.FromContextOrDiscard(ctx).Info( + "VM resource readiness probe condition updated", + "condition.status", cond.Status, + "time", cond.LastTransitionTime, + "reason", cond.Reason) + } + + conditions.Set(vm, cond) +} + +func updateProbeStatusHeartbeat( + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine) (probeResult, string) { + + chb := string(moVM.GuestHeartbeatStatus) + mhb := string(vm.Spec.ReadinessProbe.GuestHeartbeat.ThresholdStatus) + chv := heartbeatValue(chb) + if chv < 0 { + return probeResultUnknown, "" + } + if mhv := heartbeatValue(mhb); chv < mhv { + return probeResultFailure, fmt.Sprintf( + "heartbeat status %q is below threshold", chb) + } + return probeResultSuccess, "" +} + +func updateProbeStatusGuestInfo( + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine) (probeResult, string) { //nolint:unparam + + numProbes := len(vm.Spec.ReadinessProbe.GuestInfo) + if numProbes == 0 { + return probeResultUnknown, "" + } + + // Build the list of guestinfo keys. + var ( + guestInfoKeys = make([]string, numProbes) + guestInfoKeyVals = make(map[string]string, numProbes) + ) + for i := range vm.Spec.ReadinessProbe.GuestInfo { + gi := vm.Spec.ReadinessProbe.GuestInfo[i] + giKey := fmt.Sprintf("guestinfo.%s", gi.Key) + guestInfoKeys[i] = giKey + guestInfoKeyVals[giKey] = gi.Value + } + + if moVM.Config == nil { + panic("moVM.Config is nil") + } + + results := object.OptionValueList(moVM.Config.ExtraConfig).StringMap() + + for i := range guestInfoKeys { + key := guestInfoKeys[i] + expectedVal := guestInfoKeyVals[key] + + actualVal, ok := results[key] + if !ok { + return probeResultFailure, "" + } + + if expectedVal == "" { + // Matches everything. + continue + } + + expectedValRx, err := regexp.Compile(expectedVal) + if err != nil { + // Treat an invalid expressions as a wildcard too. + continue + } + + if !expectedValRx.MatchString(actualVal) { + return probeResultFailure, "" + } + } + + return probeResultSuccess, "" +} diff --git a/pkg/providers/vsphere/vmlifecycle/update_status_test.go b/pkg/providers/vsphere/vmlifecycle/update_status_test.go index 65625539e..692bedb4a 100644 --- a/pkg/providers/vsphere/vmlifecycle/update_status_test.go +++ b/pkg/providers/vsphere/vmlifecycle/update_status_test.go @@ -14,6 +14,7 @@ import ( "github.com/vmware/govmomi/vim25/mo" vimtypes "github.com/vmware/govmomi/vim25/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apirecord "k8s.io/client-go/tools/record" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" vmopv1common "github.com/vmware-tanzu/vm-operator/api/v1alpha3/common" @@ -23,6 +24,7 @@ import ( "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/network" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/vmlifecycle" + "github.com/vmware-tanzu/vm-operator/pkg/record" "github.com/vmware-tanzu/vm-operator/pkg/util" "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" "github.com/vmware-tanzu/vm-operator/test/builder" @@ -467,601 +469,823 @@ var _ = Describe("UpdateStatus", func() { }) }) - Context("Storage", func() { - const oneGiBInBytes = 1 /* B */ * 1024 /* KiB */ * 1024 /* MiB */ * 1024 /* GiB */ + }) + + Context("Storage", func() { + const oneGiBInBytes = 1 /* B */ * 1024 /* KiB */ * 1024 /* MiB */ * 1024 /* GiB */ - Context("status.changeBlockTracking", func() { - When("moVM.config is nil", func() { + Context("status.changeBlockTracking", func() { + When("moVM.config is nil", func() { + BeforeEach(func() { + vmCtx.MoVM.Config = nil + }) + Specify("status.changeBlockTracking is nil", func() { + Expect(vmCtx.VM.Status.ChangeBlockTracking).To(BeNil()) + }) + }) + When("moVM.config is not nil", func() { + When("moVM.config.changeTrackingEnabled is nil", func() { BeforeEach(func() { - vmCtx.MoVM.Config = nil + vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{ + ChangeTrackingEnabled: nil, + } }) Specify("status.changeBlockTracking is nil", func() { Expect(vmCtx.VM.Status.ChangeBlockTracking).To(BeNil()) }) }) - When("moVM.config is not nil", func() { - When("moVM.config.changeTrackingEnabled is nil", func() { - BeforeEach(func() { - vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{ - ChangeTrackingEnabled: nil, - } - }) - Specify("status.changeBlockTracking is nil", func() { - Expect(vmCtx.VM.Status.ChangeBlockTracking).To(BeNil()) - }) + When("moVM.config.changeTrackingEnabled is true", func() { + BeforeEach(func() { + vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{ + ChangeTrackingEnabled: ptr.To(true), + } }) - When("moVM.config.changeTrackingEnabled is true", func() { - BeforeEach(func() { - vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{ - ChangeTrackingEnabled: ptr.To(true), - } - }) - Specify("status.changeBlockTracking is true", func() { - Expect(vmCtx.VM.Status.ChangeBlockTracking).To(Equal(ptr.To(true))) - }) + Specify("status.changeBlockTracking is true", func() { + Expect(vmCtx.VM.Status.ChangeBlockTracking).To(Equal(ptr.To(true))) }) - When("moVM.config.changeTrackingEnabled is false", func() { - BeforeEach(func() { - vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{ - ChangeTrackingEnabled: ptr.To(false), - } - }) - Specify("status.changeBlockTracking is false", func() { - Expect(vmCtx.VM.Status.ChangeBlockTracking).To(Equal(ptr.To(false))) - }) + }) + When("moVM.config.changeTrackingEnabled is false", func() { + BeforeEach(func() { + vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{ + ChangeTrackingEnabled: ptr.To(false), + } + }) + Specify("status.changeBlockTracking is false", func() { + Expect(vmCtx.VM.Status.ChangeBlockTracking).To(Equal(ptr.To(false))) }) }) }) + }) - Context("status.storage", func() { - When("moVM.summary.storage is nil", func() { + Context("status.storage", func() { + When("moVM.summary.storage is nil", func() { + BeforeEach(func() { + vmCtx.MoVM.Summary.Storage = nil + }) + When("status.storage is nil", func() { BeforeEach(func() { - vmCtx.MoVM.Summary.Storage = nil + vmCtx.VM.Status.Storage = nil }) - When("status.storage is nil", func() { - BeforeEach(func() { - vmCtx.VM.Status.Storage = nil - }) - Specify("status.storage to be unchanged", func() { - Expect(vmCtx.VM.Status.Storage).To(BeNil()) - }) + Specify("status.storage to be unchanged", func() { + Expect(vmCtx.VM.Status.Storage).To(BeNil()) }) - When("status.storage is not nil", func() { - BeforeEach(func() { - vmCtx.VM.Status.Storage = &vmopv1.VirtualMachineStorageStatus{} - }) - Specify("status.storage to be unchanged", func() { - Expect(vmCtx.VM.Status.Storage).To(Equal(&vmopv1.VirtualMachineStorageStatus{})) - }) + }) + When("status.storage is not nil", func() { + BeforeEach(func() { + vmCtx.VM.Status.Storage = &vmopv1.VirtualMachineStorageStatus{} }) + Specify("status.storage to be unchanged", func() { + Expect(vmCtx.VM.Status.Storage).To(Equal(&vmopv1.VirtualMachineStorageStatus{})) + }) + }) + }) + When("moVM.summary.storage is not nil", func() { + BeforeEach(func() { + vmCtx.MoVM.Summary.Storage = &vimtypes.VirtualMachineStorageSummary{ + Committed: 10 * oneGiBInBytes, + Uncommitted: 20 * oneGiBInBytes, + Unshared: 5 * oneGiBInBytes, + } }) - When("moVM.summary.storage is not nil", func() { + When("status.storage is nil", func() { BeforeEach(func() { - vmCtx.MoVM.Summary.Storage = &vimtypes.VirtualMachineStorageSummary{ - Committed: 10 * oneGiBInBytes, - Uncommitted: 20 * oneGiBInBytes, - Unshared: 5 * oneGiBInBytes, - } + vmCtx.VM.Status.Storage = nil }) - When("status.storage is nil", func() { - BeforeEach(func() { - vmCtx.VM.Status.Storage = nil - }) - Specify("status.storage to be initialized", func() { - Expect(vmCtx.VM.Status.Storage).To(Equal(&vmopv1.VirtualMachineStorageStatus{ - Committed: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), - Uncommitted: vmlifecycle.BytesToResourceGiB(20 * oneGiBInBytes), - Unshared: vmlifecycle.BytesToResourceGiB(5 * oneGiBInBytes), - })) - }) + Specify("status.storage to be initialized", func() { + Expect(vmCtx.VM.Status.Storage).To(Equal(&vmopv1.VirtualMachineStorageStatus{ + Committed: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), + Uncommitted: vmlifecycle.BytesToResourceGiB(20 * oneGiBInBytes), + Unshared: vmlifecycle.BytesToResourceGiB(5 * oneGiBInBytes), + })) }) - When("status.storage is not nil", func() { - BeforeEach(func() { - vmCtx.VM.Status.Storage = &vmopv1.VirtualMachineStorageStatus{ - Committed: vmlifecycle.BytesToResourceGiB(5 * oneGiBInBytes), - Uncommitted: vmlifecycle.BytesToResourceGiB(6 * oneGiBInBytes), - Unshared: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), - } - }) - Specify("status.storage to be updated", func() { - Expect(vmCtx.VM.Status.Storage).To(Equal(&vmopv1.VirtualMachineStorageStatus{ - Committed: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), - Uncommitted: vmlifecycle.BytesToResourceGiB(20 * oneGiBInBytes), - Unshared: vmlifecycle.BytesToResourceGiB(5 * oneGiBInBytes), - })) - }) + }) + When("status.storage is not nil", func() { + BeforeEach(func() { + vmCtx.VM.Status.Storage = &vmopv1.VirtualMachineStorageStatus{ + Committed: vmlifecycle.BytesToResourceGiB(5 * oneGiBInBytes), + Uncommitted: vmlifecycle.BytesToResourceGiB(6 * oneGiBInBytes), + Unshared: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), + } + }) + Specify("status.storage to be updated", func() { + Expect(vmCtx.VM.Status.Storage).To(Equal(&vmopv1.VirtualMachineStorageStatus{ + Committed: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), + Uncommitted: vmlifecycle.BytesToResourceGiB(20 * oneGiBInBytes), + Unshared: vmlifecycle.BytesToResourceGiB(5 * oneGiBInBytes), + })) }) }) }) + }) - Context("status.volumes", func() { - BeforeEach(func() { - vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{ - Hardware: vimtypes.VirtualHardware{ - Device: []vimtypes.BaseVirtualDevice{ - // classic - &vimtypes.VirtualDisk{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualDiskFlatVer2BackingInfo{ - VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ - FileName: "[datastore] vm/my-disk-100.vmdk", - }, - Uuid: "100", - KeyId: &vimtypes.CryptoKeyId{ - KeyId: "my-key-id", - ProviderId: &vimtypes.KeyProviderId{ - Id: "my-provider-id", - }, + Context("status.volumes", func() { + BeforeEach(func() { + vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{ + Hardware: vimtypes.VirtualHardware{ + Device: []vimtypes.BaseVirtualDevice{ + // classic + &vimtypes.VirtualDisk{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualDiskFlatVer2BackingInfo{ + VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ + FileName: "[datastore] vm/my-disk-100.vmdk", + }, + Uuid: "100", + KeyId: &vimtypes.CryptoKeyId{ + KeyId: "my-key-id", + ProviderId: &vimtypes.KeyProviderId{ + Id: "my-provider-id", }, }, - Key: 100, }, - CapacityInBytes: 10 * oneGiBInBytes, + Key: 100, }, - // classic - &vimtypes.VirtualDisk{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualDiskSeSparseBackingInfo{ - VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ - FileName: "[datastore] vm/my-disk-101.vmdk", - }, - Uuid: "101", + CapacityInBytes: 10 * oneGiBInBytes, + }, + // classic + &vimtypes.VirtualDisk{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualDiskSeSparseBackingInfo{ + VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ + FileName: "[datastore] vm/my-disk-101.vmdk", }, - Key: 101, + Uuid: "101", }, - CapacityInBytes: 1 * oneGiBInBytes, + Key: 101, }, - // classic - &vimtypes.VirtualDisk{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualDiskRawDiskMappingVer1BackingInfo{ - VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ - FileName: "[datastore] vm/my-disk-102.vmdk", - }, - Uuid: "102", + CapacityInBytes: 1 * oneGiBInBytes, + }, + // classic + &vimtypes.VirtualDisk{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualDiskRawDiskMappingVer1BackingInfo{ + VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ + FileName: "[datastore] vm/my-disk-102.vmdk", }, - Key: 102, + Uuid: "102", }, - CapacityInBytes: 2 * oneGiBInBytes, + Key: 102, }, - // classic - &vimtypes.VirtualDisk{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualDiskSparseVer2BackingInfo{ - VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ - FileName: "[datastore] vm/my-disk-103.vmdk", - }, - Uuid: "103", + CapacityInBytes: 2 * oneGiBInBytes, + }, + // classic + &vimtypes.VirtualDisk{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualDiskSparseVer2BackingInfo{ + VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ + FileName: "[datastore] vm/my-disk-103.vmdk", }, - Key: 103, + Uuid: "103", }, - CapacityInBytes: 3 * oneGiBInBytes, + Key: 103, }, - // classic - &vimtypes.VirtualDisk{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualDiskRawDiskVer2BackingInfo{ - DescriptorFileName: "[datastore] vm/my-disk-104.vmdk", - Uuid: "104", - }, - Key: 104, + CapacityInBytes: 3 * oneGiBInBytes, + }, + // classic + &vimtypes.VirtualDisk{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualDiskRawDiskVer2BackingInfo{ + DescriptorFileName: "[datastore] vm/my-disk-104.vmdk", + Uuid: "104", }, - CapacityInBytes: 4 * oneGiBInBytes, + Key: 104, }, - // managed - &vimtypes.VirtualDisk{ - VirtualDevice: vimtypes.VirtualDevice{ - Backing: &vimtypes.VirtualDiskSeSparseBackingInfo{ - VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ - FileName: "[datastore] vm/my-disk-105.vmdk", - }, - Uuid: "105", - KeyId: &vimtypes.CryptoKeyId{ - KeyId: "my-key-id", - ProviderId: &vimtypes.KeyProviderId{ - Id: "my-provider-id", - }, + CapacityInBytes: 4 * oneGiBInBytes, + }, + // managed + &vimtypes.VirtualDisk{ + VirtualDevice: vimtypes.VirtualDevice{ + Backing: &vimtypes.VirtualDiskSeSparseBackingInfo{ + VirtualDeviceFileBackingInfo: vimtypes.VirtualDeviceFileBackingInfo{ + FileName: "[datastore] vm/my-disk-105.vmdk", + }, + Uuid: "105", + KeyId: &vimtypes.CryptoKeyId{ + KeyId: "my-key-id", + ProviderId: &vimtypes.KeyProviderId{ + Id: "my-provider-id", }, }, - Key: 105, - }, - VDiskId: &vimtypes.ID{ - Id: "my-fcd-1", }, + Key: 105, + }, + VDiskId: &vimtypes.ID{ + Id: "my-fcd-1", }, }, }, - } - vmCtx.MoVM.LayoutEx = &vimtypes.VirtualMachineFileLayoutEx{ - Disk: []vimtypes.VirtualMachineFileLayoutExDiskLayout{ - { - Key: 100, - Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ - { - FileKey: []int32{0, 10}, - }, + }, + } + vmCtx.MoVM.LayoutEx = &vimtypes.VirtualMachineFileLayoutEx{ + Disk: []vimtypes.VirtualMachineFileLayoutExDiskLayout{ + { + Key: 100, + Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ + { + FileKey: []int32{0, 10}, }, }, - { - Key: 101, - Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ - { - FileKey: []int32{1, 11}, - }, + }, + { + Key: 101, + Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ + { + FileKey: []int32{1, 11}, }, }, - { - Key: 102, - Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ - { - FileKey: []int32{2, 12}, - }, + }, + { + Key: 102, + Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ + { + FileKey: []int32{2, 12}, }, }, - { - Key: 103, - Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ - { - FileKey: []int32{3, 13}, - }, + }, + { + Key: 103, + Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ + { + FileKey: []int32{3, 13}, }, }, - { - Key: 104, - Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ - { - FileKey: []int32{4, 14}, - }, + }, + { + Key: 104, + Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ + { + FileKey: []int32{4, 14}, }, }, - { - Key: 105, - Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ - { - FileKey: []int32{5, 15}, - }, + }, + { + Key: 105, + Chain: []vimtypes.VirtualMachineFileLayoutExDiskUnit{ + { + FileKey: []int32{5, 15}, }, }, }, - File: []vimtypes.VirtualMachineFileLayoutExFileInfo{ - { - Key: 0, - Size: 500, - UniqueSize: 500, - }, - { - Key: 10, - Size: 2 * oneGiBInBytes, - UniqueSize: 1 * oneGiBInBytes, - }, + }, + File: []vimtypes.VirtualMachineFileLayoutExFileInfo{ + { + Key: 0, + Size: 500, + UniqueSize: 500, + }, + { + Key: 10, + Size: 2 * oneGiBInBytes, + UniqueSize: 1 * oneGiBInBytes, + }, - { - Key: 1, - Size: 500, - UniqueSize: 500, - }, - { - Key: 11, - Size: 0.5 * oneGiBInBytes, - UniqueSize: 0.25 * oneGiBInBytes, - }, + { + Key: 1, + Size: 500, + UniqueSize: 500, + }, + { + Key: 11, + Size: 0.5 * oneGiBInBytes, + UniqueSize: 0.25 * oneGiBInBytes, + }, - { - Key: 2, - Size: 500, - UniqueSize: 500, - }, - { - Key: 12, - Size: 1 * oneGiBInBytes, - UniqueSize: 0.5 * oneGiBInBytes, - }, + { + Key: 2, + Size: 500, + UniqueSize: 500, + }, + { + Key: 12, + Size: 1 * oneGiBInBytes, + UniqueSize: 0.5 * oneGiBInBytes, + }, - { - Key: 3, - Size: 500, - UniqueSize: 500, - }, - { - Key: 13, - Size: 2 * oneGiBInBytes, - UniqueSize: 1 * oneGiBInBytes, - }, + { + Key: 3, + Size: 500, + UniqueSize: 500, + }, + { + Key: 13, + Size: 2 * oneGiBInBytes, + UniqueSize: 1 * oneGiBInBytes, + }, - { - Key: 4, - Size: 500, - UniqueSize: 500, - }, - { - Key: 14, - Size: 3 * oneGiBInBytes, - UniqueSize: 2 * oneGiBInBytes, + { + Key: 4, + Size: 500, + UniqueSize: 500, + }, + { + Key: 14, + Size: 3 * oneGiBInBytes, + UniqueSize: 2 * oneGiBInBytes, + }, + + { + Key: 5, + Size: 500, + UniqueSize: 500, + }, + { + Key: 15, + Size: 50 * oneGiBInBytes, + UniqueSize: 50 * oneGiBInBytes, + }, + }, + } + vmCtx.VM.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{} + }) + When("moVM.config is nil", func() { + BeforeEach(func() { + vmCtx.MoVM.Config = nil + }) + Specify("status.volumes is unchanged", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + }) + }) + When("moVM.config.hardware.device is empty", func() { + BeforeEach(func() { + vmCtx.MoVM.Config.Hardware.Device = nil + }) + Specify("status.volumes is unchanged", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + }) + }) + When("moVM.layoutEx is nil", func() { + BeforeEach(func() { + vmCtx.MoVM.LayoutEx = nil + }) + Specify("status.volumes is unchanged", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + }) + }) + When("moVM.layoutEx.disk is empty", func() { + BeforeEach(func() { + vmCtx.MoVM.LayoutEx.Disk = nil + }) + Specify("status.volumes is unchanged", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + }) + }) + When("moVM.layoutEx.file is empty", func() { + BeforeEach(func() { + vmCtx.MoVM.LayoutEx.Disk = nil + }) + Specify("status.volumes is unchanged", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + }) + }) + When("vm.status.volumes does not have pvcs", func() { + Specify("status.volumes includes the classic disks", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{ + { + Name: "my-disk-100", + DiskUUID: "100", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Crypto: &vmopv1.VirtualMachineVolumeCryptoStatus{ + KeyID: "my-key-id", + ProviderID: "my-provider-id", }, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + }, + { + Name: "my-disk-101", + DiskUUID: "101", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(1 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (0.25 * oneGiBInBytes)), + }, + { + Name: "my-disk-102", + DiskUUID: "102", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (0.5 * oneGiBInBytes)), + }, + { + Name: "my-disk-103", + DiskUUID: "103", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(3 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + }, + { + Name: "my-disk-104", + DiskUUID: "104", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(4 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (2 * oneGiBInBytes)), + }, + })) + }) + }) - { - Key: 5, - Size: 500, - UniqueSize: 500, + When("vm.status.volumes has a pvc", func() { + BeforeEach(func() { + vmCtx.VM.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ + { + Name: "my-disk-105", + DiskUUID: "105", + Type: vmopv1.VirtualMachineStorageDiskTypeManaged, + Attached: false, + Limit: vmlifecycle.BytesToResourceGiB(100 * oneGiBInBytes), + }, + } + }) + Specify("status.volumes includes the pvc and classic disks", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{ + { + Name: "my-disk-100", + DiskUUID: "100", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Crypto: &vmopv1.VirtualMachineVolumeCryptoStatus{ + KeyID: "my-key-id", + ProviderID: "my-provider-id", }, - { - Key: 15, - Size: 50 * oneGiBInBytes, - UniqueSize: 50 * oneGiBInBytes, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + }, + { + Name: "my-disk-101", + DiskUUID: "101", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(1 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (0.25 * oneGiBInBytes)), + }, + { + Name: "my-disk-102", + DiskUUID: "102", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (0.5 * oneGiBInBytes)), + }, + { + Name: "my-disk-103", + DiskUUID: "103", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(3 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + }, + { + Name: "my-disk-104", + DiskUUID: "104", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(4 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (2 * oneGiBInBytes)), + }, + { + Name: "my-disk-105", + DiskUUID: "105", + Type: vmopv1.VirtualMachineStorageDiskTypeManaged, + Crypto: &vmopv1.VirtualMachineVolumeCryptoStatus{ + KeyID: "my-key-id", + ProviderID: "my-provider-id", }, + Attached: false, + Limit: vmlifecycle.BytesToResourceGiB(100 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (50 * oneGiBInBytes)), + }, + })) + }) + }) + + When("vm.status.volumes has a stale classic disk", func() { + BeforeEach(func() { + vmCtx.VM.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ + { + Name: "my-disk-106", + DiskUUID: "106", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), }, } - vmCtx.VM.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{} }) - When("moVM.config is nil", func() { + Specify("status.volumes no longer includes the stale classic disk", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{ + { + Name: "my-disk-100", + DiskUUID: "100", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Crypto: &vmopv1.VirtualMachineVolumeCryptoStatus{ + ProviderID: "my-provider-id", + KeyID: "my-key-id", + }, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + }, + { + Name: "my-disk-101", + DiskUUID: "101", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(1 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (0.25 * oneGiBInBytes)), + }, + { + Name: "my-disk-102", + DiskUUID: "102", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (0.5 * oneGiBInBytes)), + }, + { + Name: "my-disk-103", + DiskUUID: "103", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(3 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + }, + { + Name: "my-disk-104", + DiskUUID: "104", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(4 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (2 * oneGiBInBytes)), + }, + })) + }) + }) + + When("there is a classic disk w an invalid path", func() { + BeforeEach(func() { + vmCtx.MoVM.Config.Hardware. + Device[0].(*vimtypes.VirtualDisk). + Backing.(*vimtypes.VirtualDiskFlatVer2BackingInfo). + FileName = "invalid" + }) + Specify("status.volumes omits the classic disk w invalid path", func() { + Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{ + { + Name: "my-disk-101", + DiskUUID: "101", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(1 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (0.25 * oneGiBInBytes)), + }, + { + Name: "my-disk-102", + DiskUUID: "102", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (0.5 * oneGiBInBytes)), + }, + { + Name: "my-disk-103", + DiskUUID: "103", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(3 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + }, + { + Name: "my-disk-104", + DiskUUID: "104", + Type: vmopv1.VirtualMachineStorageDiskTypeClassic, + Attached: true, + Limit: vmlifecycle.BytesToResourceGiB(4 * oneGiBInBytes), + Used: vmlifecycle.BytesToResourceGiB(500 + (2 * oneGiBInBytes)), + }, + })) + }) + }) + }) + }) + + Context("ReadinessProbe", func() { + + var ( + chanRecord chan string + ) + + BeforeEach(func() { + chanRecord = make(chan string, 10) + + vmCtx.Context = record.WithContext( + vmCtx.Context, + record.New(&apirecord.FakeRecorder{Events: chanRecord})) + + pkgcfg.SetContext(vmCtx, func(config *pkgcfg.Config) { + config.Features.WorkloadDomainIsolation = true + config.AsyncSignalDisabled = false + }) + }) + + assertEvent := func(msg string) { + var e string + EventuallyWithOffset(1, chanRecord).Should(Receive(&e, Equal(msg))) + } + + When("there is no probe", func() { + BeforeEach(func() { + vmCtx.VM.Spec.ReadinessProbe = nil + }) + It("should not update status", func() { + Expect(conditions.Has(vmCtx.VM, vmopv1.ReadyConditionType)).To(BeFalse()) + }) + }) + + When("there is a TCP probe", func() { + BeforeEach(func() { + vmCtx.VM.Spec.ReadinessProbe = &vmopv1.VirtualMachineReadinessProbeSpec{ + TCPSocket: &vmopv1.TCPSocketAction{}, + } + }) + It("should not update status", func() { + Expect(conditions.Has(vmCtx.VM, vmopv1.ReadyConditionType)).To(BeFalse()) + }) + }) + + When("there is a GuestHeartbeat probe", func() { + BeforeEach(func() { + vmCtx.VM.Spec.ReadinessProbe = &vmopv1.VirtualMachineReadinessProbeSpec{ + GuestHeartbeat: &vmopv1.GuestHeartbeatAction{}, + } + }) + When("threshold is green", func() { + BeforeEach(func() { + vmCtx.VM.Spec.ReadinessProbe.GuestHeartbeat.ThresholdStatus = vmopv1.GreenHeartbeatStatus + }) + When("vm is green", func() { BeforeEach(func() { - vmCtx.MoVM.Config = nil + vmCtx.MoVM.GuestHeartbeatStatus = vimtypes.ManagedEntityStatusGreen }) - Specify("status.volumes is unchanged", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + It("should mark ready=true", func() { + Expect(conditions.IsTrue(vmCtx.VM, vmopv1.ReadyConditionType)).To(BeTrue()) + assertEvent("Normal Ready ") }) }) - When("moVM.config.hardware.device is empty", func() { + When("vm is red", func() { BeforeEach(func() { - vmCtx.MoVM.Config.Hardware.Device = nil + vmCtx.MoVM.GuestHeartbeatStatus = vimtypes.ManagedEntityStatusRed }) - Specify("status.volumes is unchanged", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + It("should mark ready=false", func() { + c := conditions.Get(vmCtx.VM, vmopv1.ReadyConditionType) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal("NotReady")) + Expect(c.Message).To(Equal("heartbeat status \"red\" is below threshold")) + assertEvent("Normal NotReady heartbeat status \"red\" is below threshold") }) }) - When("moVM.layoutEx is nil", func() { + When("vm is unknown color", func() { BeforeEach(func() { - vmCtx.MoVM.LayoutEx = nil + vmCtx.MoVM.GuestHeartbeatStatus = "unknown" }) - Specify("status.volumes is unchanged", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + It("should mark ready=Unknown", func() { + c := conditions.Get(vmCtx.VM, vmopv1.ReadyConditionType) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionUnknown)) + assertEvent("Normal Unknown ") }) }) - When("moVM.layoutEx.disk is empty", func() { + }) + }) + + When("there is a GuestInfo probe", func() { + BeforeEach(func() { + vmCtx.MoVM.Config = &vimtypes.VirtualMachineConfigInfo{} + }) + When("specific match", func() { + BeforeEach(func() { + vmCtx.VM.Spec.ReadinessProbe = &vmopv1.VirtualMachineReadinessProbeSpec{ + GuestInfo: []vmopv1.GuestInfoAction{ + { + Key: "hello", + Value: "^world$", + }, + }, + } + }) + When("match exists", func() { BeforeEach(func() { - vmCtx.MoVM.LayoutEx.Disk = nil + vmCtx.MoVM.Config.ExtraConfig = []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: "guestinfo.hello", + Value: "world", + }, + } }) - Specify("status.volumes is unchanged", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + It("should mark ready=true", func() { + Expect(conditions.IsTrue(vmCtx.VM, vmopv1.ReadyConditionType)).To(BeTrue()) + assertEvent("Normal Ready ") }) }) - When("moVM.layoutEx.file is empty", func() { - BeforeEach(func() { - vmCtx.MoVM.LayoutEx.Disk = nil - }) - Specify("status.volumes is unchanged", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{})) + + When("match does not exist", func() { + It("should mark ready=false", func() { + c := conditions.Get(vmCtx.VM, vmopv1.ReadyConditionType) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal("NotReady")) + assertEvent("Normal NotReady ") }) }) - When("vm.status.volumes does not have pvcs", func() { - Specify("status.volumes includes the classic disks", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{ - { - Name: "my-disk-100", - DiskUUID: "100", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Crypto: &vmopv1.VirtualMachineVolumeCryptoStatus{ - KeyID: "my-key-id", - ProviderID: "my-provider-id", - }, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), - }, - { - Name: "my-disk-101", - DiskUUID: "101", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(1 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (0.25 * oneGiBInBytes)), - }, - { - Name: "my-disk-102", - DiskUUID: "102", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (0.5 * oneGiBInBytes)), - }, - { - Name: "my-disk-103", - DiskUUID: "103", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(3 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), - }, + }) + When("wildcard match", func() { + BeforeEach(func() { + vmCtx.VM.Spec.ReadinessProbe = &vmopv1.VirtualMachineReadinessProbeSpec{ + GuestInfo: []vmopv1.GuestInfoAction{ { - Name: "my-disk-104", - DiskUUID: "104", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(4 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (2 * oneGiBInBytes)), + Key: "hello", }, - })) - }) + }, + } }) - - When("vm.status.volumes has a pvc", func() { + When("match exists", func() { BeforeEach(func() { - vmCtx.VM.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ - { - Name: "my-disk-105", - DiskUUID: "105", - Type: vmopv1.VirtualMachineStorageDiskTypeManaged, - Attached: false, - Limit: vmlifecycle.BytesToResourceGiB(100 * oneGiBInBytes), + vmCtx.MoVM.Config.ExtraConfig = []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: "guestinfo.hello", + Value: "there", }, } }) - Specify("status.volumes includes the pvc and classic disks", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{ - { - Name: "my-disk-100", - DiskUUID: "100", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Crypto: &vmopv1.VirtualMachineVolumeCryptoStatus{ - KeyID: "my-key-id", - ProviderID: "my-provider-id", - }, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), - }, - { - Name: "my-disk-101", - DiskUUID: "101", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(1 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (0.25 * oneGiBInBytes)), - }, - { - Name: "my-disk-102", - DiskUUID: "102", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (0.5 * oneGiBInBytes)), - }, - { - Name: "my-disk-103", - DiskUUID: "103", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(3 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), - }, - { - Name: "my-disk-104", - DiskUUID: "104", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(4 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (2 * oneGiBInBytes)), - }, - { - Name: "my-disk-105", - DiskUUID: "105", - Type: vmopv1.VirtualMachineStorageDiskTypeManaged, - Crypto: &vmopv1.VirtualMachineVolumeCryptoStatus{ - KeyID: "my-key-id", - ProviderID: "my-provider-id", - }, - Attached: false, - Limit: vmlifecycle.BytesToResourceGiB(100 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (50 * oneGiBInBytes)), - }, - })) + It("should mark ready=true", func() { + Expect(conditions.IsTrue(vmCtx.VM, vmopv1.ReadyConditionType)).To(BeTrue()) + assertEvent("Normal Ready ") }) }) - When("vm.status.volumes has a stale classic disk", func() { - BeforeEach(func() { - vmCtx.VM.Status.Volumes = []vmopv1.VirtualMachineVolumeStatus{ - { - Name: "my-disk-106", - DiskUUID: "106", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), - }, - } + When("match does not exist", func() { + It("should mark ready=false", func() { + c := conditions.Get(vmCtx.VM, vmopv1.ReadyConditionType) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal("NotReady")) + assertEvent("Normal NotReady ") }) - Specify("status.volumes no longer includes the stale classic disk", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{ - { - Name: "my-disk-100", - DiskUUID: "100", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Crypto: &vmopv1.VirtualMachineVolumeCryptoStatus{ - ProviderID: "my-provider-id", - KeyID: "my-key-id", - }, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(10 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), - }, + }) + }) + + When("multiple actions", func() { + BeforeEach(func() { + vmCtx.VM.Spec.ReadinessProbe = &vmopv1.VirtualMachineReadinessProbeSpec{ + GuestInfo: []vmopv1.GuestInfoAction{ { - Name: "my-disk-101", - DiskUUID: "101", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(1 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (0.25 * oneGiBInBytes)), + Key: "hello", + Value: "world|there", }, { - Name: "my-disk-102", - DiskUUID: "102", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (0.5 * oneGiBInBytes)), + Key: "fu", + Value: "bar", }, - { - Name: "my-disk-103", - DiskUUID: "103", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(3 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + }, + } + }) + When("match exists", func() { + BeforeEach(func() { + vmCtx.MoVM.Config.ExtraConfig = []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: "guestinfo.hello", + Value: "world", }, - { - Name: "my-disk-104", - DiskUUID: "104", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(4 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (2 * oneGiBInBytes)), + &vimtypes.OptionValue{ + Key: "guestinfo.fu", + Value: "high bar", }, - })) + } + }) + It("should mark ready=true", func() { + Expect(conditions.IsTrue(vmCtx.VM, vmopv1.ReadyConditionType)).To(BeTrue()) + assertEvent("Normal Ready ") }) }) - When("there is a classic disk w an invalid path", func() { + When("match does not exist", func() { BeforeEach(func() { - vmCtx.MoVM.Config.Hardware. - Device[0].(*vimtypes.VirtualDisk). - Backing.(*vimtypes.VirtualDiskFlatVer2BackingInfo). - FileName = "invalid" - }) - Specify("status.volumes omits the classic disk w invalid path", func() { - Expect(vmCtx.VM.Status.Volumes).To(Equal([]vmopv1.VirtualMachineVolumeStatus{ - { - Name: "my-disk-101", - DiskUUID: "101", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(1 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (0.25 * oneGiBInBytes)), - }, - { - Name: "my-disk-102", - DiskUUID: "102", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(2 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (0.5 * oneGiBInBytes)), - }, - { - Name: "my-disk-103", - DiskUUID: "103", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(3 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (1 * oneGiBInBytes)), + vmCtx.MoVM.Config.ExtraConfig = []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: "guestinfo.hello", + Value: "there", }, - { - Name: "my-disk-104", - DiskUUID: "104", - Type: vmopv1.VirtualMachineStorageDiskTypeClassic, - Attached: true, - Limit: vmlifecycle.BytesToResourceGiB(4 * oneGiBInBytes), - Used: vmlifecycle.BytesToResourceGiB(500 + (2 * oneGiBInBytes)), - }, - })) + } + }) + It("should mark ready=false", func() { + c := conditions.Get(vmCtx.VM, vmopv1.ReadyConditionType) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal("NotReady")) + assertEvent("Normal NotReady ") }) }) }) diff --git a/pkg/record/recorder_context.go b/pkg/record/recorder_context.go new file mode 100644 index 000000000..8f9c443bb --- /dev/null +++ b/pkg/record/recorder_context.go @@ -0,0 +1,37 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package record + +import ( + "context" + + ctxgen "github.com/vmware-tanzu/vm-operator/pkg/context/generic" +) + +type contextKeyType uint8 + +const contextKeyValue contextKeyType = 0 + +type contextValueType = Recorder + +// FromContext returns the recorder from the specified context. +func FromContext(ctx context.Context) contextValueType { + return ctxgen.FromContext( + ctx, + contextKeyValue, + func(val contextValueType) contextValueType { + return val + }) +} + +// WithContext returns a new recorder context. +func WithContext(parent context.Context, val contextValueType) context.Context { + if val == nil { + panic("recorder is nil") + } + return ctxgen.WithContext( + parent, + contextKeyValue, + func() contextValueType { return val }) +} diff --git a/pkg/record/recorder_context_test.go b/pkg/record/recorder_context_test.go new file mode 100644 index 000000000..23cf9689f --- /dev/null +++ b/pkg/record/recorder_context_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package record_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + apirecord "k8s.io/client-go/tools/record" + + "github.com/vmware-tanzu/vm-operator/pkg/record" +) + +var _ = Describe("WithContext", func() { + var ( + left context.Context + leftVal record.Recorder + ) + BeforeEach(func() { + left = context.Background() + leftVal = record.New(apirecord.NewFakeRecorder(0)) + }) + When("parent is nil", func() { + BeforeEach(func() { + left = nil + }) + It("should panic", func() { + fn := func() { + _ = record.WithContext(left, leftVal) + } + Expect(fn).To(PanicWith("parent context is nil")) + }) + }) + When("parent is not nil", func() { + When("value is nil", func() { + BeforeEach(func() { + leftVal = nil + }) + It("should panic", func() { + fn := func() { + _ = record.WithContext(left, leftVal) + } + Expect(fn).To(PanicWith("recorder is nil")) + }) + }) + When("value is not nil", func() { + It("should return a new context", func() { + ctx := record.WithContext(left, leftVal) + Expect(ctx).ToNot(BeNil()) + Expect(record.FromContext(ctx)).To(Equal(leftVal)) + }) + }) + }) +}) diff --git a/pkg/util/vsphere/watcher/watcher.go b/pkg/util/vsphere/watcher/watcher.go index 2f7f97a72..1dc9bfff9 100644 --- a/pkg/util/vsphere/watcher/watcher.go +++ b/pkg/util/vsphere/watcher/watcher.go @@ -29,6 +29,7 @@ func DefaultWatchedPropertyPaths() []string { "config.keyId", "guest.ipStack", "guest.net", + "guestHeartbeatStatus", "summary.config.name", "summary.guest", "summary.overallStatus", diff --git a/test/builder/vcsim_test_context.go b/test/builder/vcsim_test_context.go index 276afce6d..82c5d6943 100644 --- a/test/builder/vcsim_test_context.go +++ b/test/builder/vcsim_test_context.go @@ -297,6 +297,8 @@ func newTestContextForVCSim( fakeRecorder, _ := NewFakeRecorder() + parentCtx = record.WithContext(parentCtx, fakeRecorder) + ctx := &TestContextForVCSim{ UnitTestContext: NewUnitTestContextWithParentContext(parentCtx, initObjects...), PodNamespace: "vmop-pod-test", @@ -312,6 +314,7 @@ func newTestContextForVCSim( ctx.ClustersPerZone = clustersPerZone // TODO: this can be removed once FSS_WCP_WORKLOAD_DOMAIN_ISOLATION enabled. ctx.withWorkloadIsolation = config.WithWorkloadIsolation + return ctx }