diff --git a/api/v1alpha1/virtualmachine_conversion.go b/api/v1alpha1/virtualmachine_conversion.go index ef2a6e9e0..3d1a02906 100644 --- a/api/v1alpha1/virtualmachine_conversion.go +++ b/api/v1alpha1/virtualmachine_conversion.go @@ -822,6 +822,10 @@ func Convert_v1alpha3_VirtualMachineStatus_To_v1alpha1_VirtualMachineStatus( return nil } +func restore_v1alpha3_VirtualMachineEncryptionClass(dst, src *vmopv1.VirtualMachine) { + dst.Spec.Crypto.ClassName = src.Spec.Crypto.ClassName +} + func restore_v1alpha3_VirtualMachineImage(dst, src *vmopv1.VirtualMachine) { dst.Spec.Image = src.Spec.Image dst.Spec.ImageName = src.Spec.ImageName @@ -1239,6 +1243,7 @@ func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error { restore_v1alpha3_VirtualMachineInstanceUUID(dst, restored) restore_v1alpha3_VirtualMachineGuestID(dst, restored) restore_v1alpha3_VirtualMachineCdrom(dst, restored) + restore_v1alpha3_VirtualMachineEncryptionClass(dst, restored) // END RESTORE diff --git a/api/v1alpha1/zz_generated.conversion.go b/api/v1alpha1/zz_generated.conversion.go index 704cb6ad1..8c5628187 100644 --- a/api/v1alpha1/zz_generated.conversion.go +++ b/api/v1alpha1/zz_generated.conversion.go @@ -2052,6 +2052,7 @@ func autoConvert_v1alpha3_VirtualMachineSpec_To_v1alpha1_VirtualMachineSpec(in * // WARNING: in.Image requires manual conversion: does not exist in peer-type out.ImageName = in.ImageName out.ClassName = in.ClassName + // WARNING: in.Crypto requires manual conversion: does not exist in peer-type out.StorageClass = in.StorageClass // WARNING: in.Bootstrap requires manual conversion: does not exist in peer-type // WARNING: in.Network requires manual conversion: does not exist in peer-type diff --git a/api/v1alpha2/virtualmachine_conversion.go b/api/v1alpha2/virtualmachine_conversion.go index f2c099557..74de87c99 100644 --- a/api/v1alpha2/virtualmachine_conversion.go +++ b/api/v1alpha2/virtualmachine_conversion.go @@ -142,6 +142,10 @@ func Convert_v1alpha3_VirtualMachine_To_v1alpha2_VirtualMachine( return nil } +func restore_v1alpha3_VirtualMachineEncryptionClass(dst, src *vmopv1.VirtualMachine) { + dst.Spec.Crypto.ClassName = src.Spec.Crypto.ClassName +} + func restore_v1alpha3_VirtualMachineImage(dst, src *vmopv1.VirtualMachine) { dst.Spec.Image = src.Spec.Image dst.Spec.ImageName = src.Spec.ImageName @@ -293,6 +297,7 @@ func (src *VirtualMachine) ConvertTo(dstRaw ctrlconversion.Hub) error { restore_v1alpha3_VirtualMachineSpecNetworkDomainName(dst, restored) restore_v1alpha3_VirtualMachineGuestID(dst, restored) restore_v1alpha3_VirtualMachineCdrom(dst, restored) + restore_v1alpha3_VirtualMachineEncryptionClass(dst, restored) // END RESTORE diff --git a/api/v1alpha2/zz_generated.conversion.go b/api/v1alpha2/zz_generated.conversion.go index 91c61df5e..07475ccb9 100644 --- a/api/v1alpha2/zz_generated.conversion.go +++ b/api/v1alpha2/zz_generated.conversion.go @@ -3175,6 +3175,7 @@ func autoConvert_v1alpha3_VirtualMachineSpec_To_v1alpha2_VirtualMachineSpec(in * // WARNING: in.Image requires manual conversion: does not exist in peer-type out.ImageName = in.ImageName out.ClassName = in.ClassName + // WARNING: in.Crypto requires manual conversion: does not exist in peer-type out.StorageClass = in.StorageClass if in.Bootstrap != nil { in, out := &in.Bootstrap, &out.Bootstrap diff --git a/api/v1alpha3/virtualmachine_types.go b/api/v1alpha3/virtualmachine_types.go index 1c8ca0c31..e9cd77c9a 100644 --- a/api/v1alpha3/virtualmachine_types.go +++ b/api/v1alpha3/virtualmachine_types.go @@ -41,6 +41,14 @@ const ( // VirtualMachineConditionPlacementReady indicates that the placement decision for the VM is ready. VirtualMachineConditionPlacementReady = "VirtualMachineConditionPlacementReady" + // VirtualMachineEncryptionClassReady indicates that a referenced + // EncryptionClass is ready. + VirtualMachineEncryptionClassReady = "VirtualMachineEncryptionClassReady" + + // VirtualMachineEncryptionSynced indicates that the VirtualMachine's + // encryption state is synced to the desired encryption state. + VirtualMachineEncryptionSynced = "VirtualMachineEncryptionSynced" + // VirtualMachineConditionCreated indicates that the VM has been created. VirtualMachineConditionCreated = "VirtualMachineCreated" @@ -357,6 +365,84 @@ type VirtualMachineCdromSpec struct { AllowGuestControl *bool `json:"allowGuestControl,omitempty"` } +// +kubebuilder:validation:Enum=NoOp;DefaultKeyProvider + +// VirtualMachineCryptoFallbackMode represents the various fallback modes for +// when an encrypted VirtualMachine does not specify an encryption class. +type VirtualMachineCryptoFallbackMode string + +const ( + // VirtualMachinePowerOpModeHard indicates to halt a VM when powering it + // off or when suspending a VM to not involve the guest. + VirtualMachineCryptoFallbackModeNoOp VirtualMachineCryptoFallbackMode = "NoOp" + + // VirtualMachinePowerOpModeSoft indicates to ask VM Tools running + // inside of a VM's guest to shutdown the guest gracefully when powering + // off a VM or when suspending a VM to allow the guest to participate. + // + // If this mode is set on a VM whose guest does not have VM Tools or if + // VM Tools is present but the operation fails, the VM may never realize + // the desired power state. This can prevent a VM from being deleted as well + // as many other unexpected issues. It is recommended to use trySoft + // instead. + VirtualMachineCryptoFallbackModeDefaultKeyProvider VirtualMachineCryptoFallbackMode = "DefaultKeyProvider" +) + +// VirtualMachineCryptoSpec defines the desired state of a VirtualMachine's +// encryption state. +type VirtualMachineCryptoSpec struct { + // +optional + + // ClassName describes the name of the EncryptionClass resource + // used to encrypt this VM. + // + // Please note, this field is not required to encrypt the VM. If the + // underlying platform has a default key provider, the VM may still be fully + // or partially encrypted depending on the specified storage and VM classes. + // + // If there is a default key provider and an encryption storage class is + // selected, the VM's home files and non-PVC disks will be encrypted. + // + // If there is a default key provider and a and a VM Class with a virtual, + // trusted platform module (vTPM) is selected, the VM's home files will be + // encrypted. + // + // If the underlying vSphere platform does not have a default key provider, + // then this field is required when specifying an encryption storage class + // and/or a VM Class with a vTPM. + ClassName string `json:"className,omitempty"` + + // +optional + // +kubebuilder:default=true + + // UseDefaultKeyProvider describes the desired behavior for when an explicit + // EncryptionClass is not provided. + // + // When this value is true: + // + // - If a VirtualMachine is not encrypted, uses an encryption storage + // policy or has a virtual, trusted platform module (vTPM), there is a + // default key provider, and an EncryptionClass is not provided, the VM + // will be encrypted using the default key provider. + // + // - If a VirtualMachine was encrypted using an EncryptionClass and the + // the field spec.crypto.className is set to an empty value, the VM will + // be rekeyed using the default key provider. + // + // When this value is false: + // + // - If a VirtualMachine was encrypted using an EncryptionClass and the + // the field spec.crypto.className is set to an empty value, the VM will + // remain encrypted using its current provider and key ID. + // + // Please note, this could result in a VirtualMachine that cannot be + // powered on if the EncryptionClass was removed and its referenced, + // underlying key provider no longer exists. + // + // Defaults to true if omitted. + UseDefaultKeyProvider *bool `json:"useDefaultKeyProvider,omitempty"` +} + // VirtualMachineSpec defines the desired state of a VirtualMachine. type VirtualMachineSpec struct { // +optional @@ -439,6 +525,11 @@ type VirtualMachineSpec struct { // +optional + // Crypto describes the desired encryption state of the VirtualMachine. + Crypto VirtualMachineCryptoSpec `json:"crypto,omitempty"` + + // +optional + // StorageClass describes the name of a Kubernetes StorageClass resource // used to configure this VM's storage-related attributes. // diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go index e759a1b55..5ba6efe82 100644 --- a/api/v1alpha3/zz_generated.deepcopy.go +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -742,6 +742,26 @@ func (in *VirtualMachineClassStatus) DeepCopy() *VirtualMachineClassStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineCryptoSpec) DeepCopyInto(out *VirtualMachineCryptoSpec) { + *out = *in + if in.UseDefaultKeyProvider != nil { + in, out := &in.UseDefaultKeyProvider, &out.UseDefaultKeyProvider + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineCryptoSpec. +func (in *VirtualMachineCryptoSpec) DeepCopy() *VirtualMachineCryptoSpec { + if in == nil { + return nil + } + out := new(VirtualMachineCryptoSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualMachineImage) DeepCopyInto(out *VirtualMachineImage) { *out = *in @@ -2049,6 +2069,7 @@ func (in *VirtualMachineSpec) DeepCopyInto(out *VirtualMachineSpec) { *out = new(VirtualMachineImageRef) **out = **in } + in.Crypto.DeepCopyInto(&out.Crypto) if in.Bootstrap != nil { in, out := &in.Bootstrap, &out.Bootstrap *out = new(VirtualMachineBootstrapSpec) diff --git a/config/crd/bases/vmoperator.vmware.com_virtualmachinereplicasets.yaml b/config/crd/bases/vmoperator.vmware.com_virtualmachinereplicasets.yaml index dcdba9cf7..ea8a49a55 100644 --- a/config/crd/bases/vmoperator.vmware.com_virtualmachinereplicasets.yaml +++ b/config/crd/bases/vmoperator.vmware.com_virtualmachinereplicasets.yaml @@ -1007,6 +1007,60 @@ spec: an existing VM on the underlying platform that was not deployed from a VM class. type: string + crypto: + description: Crypto describes the desired encryption state + of the VirtualMachine. + properties: + className: + description: |- + ClassName describes the name of the EncryptionClass resource + used to encrypt this VM. + + Please note, this field is not required to encrypt the VM. If the + underlying platform has a default key provider, the VM may still be fully + or partially encrypted depending on the specified storage and VM classes. + + If there is a default key provider and an encryption storage class is + selected, the VM's home files and non-PVC disks will be encrypted. + + If there is a default key provider and a and a VM Class with a virtual, + trusted platform module (vTPM) is selected, the VM's home files will be + encrypted. + + If the underlying vSphere platform does not have a default key provider, + then this field is required when specifying an encryption storage class + and/or a VM Class with a vTPM. + type: string + useDefaultKeyProvider: + default: true + description: |- + UseDefaultKeyProvider describes the desired behavior for when an explicit + EncryptionClass is not provided. + + When this value is true: + + - If a VirtualMachine is not encrypted, uses an encryption storage + policy or has a virtual, trusted platform module (vTPM), there is a + default key provider, and an EncryptionClass is not provided, the VM + will be encrypted using the default key provider. + + - If a VirtualMachine was encrypted using an EncryptionClass and the + the field spec.crypto.className is set to an empty value, the VM will + be rekeyed using the default key provider. + + When this value is false: + + - If a VirtualMachine was encrypted using an EncryptionClass and the + the field spec.crypto.className is set to an empty value, the VM will + remain encrypted using its current provider and key ID. + + Please note, this could result in a VirtualMachine that cannot be + powered on if the EncryptionClass was removed and its referenced, + underlying key provider no longer exists. + + Defaults to true if omitted. + type: boolean + type: object guestID: description: |- GuestID describes the desired guest operating system identifier for a VM. diff --git a/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml b/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml index 232dd3631..84e046050 100644 --- a/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml +++ b/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml @@ -3791,6 +3791,60 @@ spec: an existing VM on the underlying platform that was not deployed from a VM class. type: string + crypto: + description: Crypto describes the desired encryption state of the + VirtualMachine. + properties: + className: + description: |- + ClassName describes the name of the EncryptionClass resource + used to encrypt this VM. + + Please note, this field is not required to encrypt the VM. If the + underlying platform has a default key provider, the VM may still be fully + or partially encrypted depending on the specified storage and VM classes. + + If there is a default key provider and an encryption storage class is + selected, the VM's home files and non-PVC disks will be encrypted. + + If there is a default key provider and a and a VM Class with a virtual, + trusted platform module (vTPM) is selected, the VM's home files will be + encrypted. + + If the underlying vSphere platform does not have a default key provider, + then this field is required when specifying an encryption storage class + and/or a VM Class with a vTPM. + type: string + useDefaultKeyProvider: + default: true + description: |- + UseDefaultKeyProvider describes the desired behavior for when an explicit + EncryptionClass is not provided. + + When this value is true: + + - If a VirtualMachine is not encrypted, uses an encryption storage + policy or has a virtual, trusted platform module (vTPM), there is a + default key provider, and an EncryptionClass is not provided, the VM + will be encrypted using the default key provider. + + - If a VirtualMachine was encrypted using an EncryptionClass and the + the field spec.crypto.className is set to an empty value, the VM will + be rekeyed using the default key provider. + + When this value is false: + + - If a VirtualMachine was encrypted using an EncryptionClass and the + the field spec.crypto.className is set to an empty value, the VM will + remain encrypted using its current provider and key ID. + + Please note, this could result in a VirtualMachine that cannot be + powered on if the EncryptionClass was removed and its referenced, + underlying key provider no longer exists. + + Defaults to true if omitted. + type: boolean + type: object guestID: description: |- GuestID describes the desired guest operating system identifier for a VM. diff --git a/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go b/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go index 69d0d90e5..05f963421 100644 --- a/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go +++ b/controllers/virtualmachine/virtualmachine/virtualmachine_controller.go @@ -24,6 +24,7 @@ import ( vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + 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" pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" @@ -104,6 +105,14 @@ func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) err builder = builder.Watches(&vmopv1.VirtualMachineClass{}, handler.EnqueueRequestsFromMapFunc(classToVMMapperFn(ctx, r.Client, isDefaultVMClassController))) + if pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + builder = builder.Watches( + &byokv1.EncryptionClass{}, + handler.EnqueueRequestsFromMapFunc( + encryptionClassToVMMapperFn(ctx, r.Client), + )) + } + return builder.Complete(r) } @@ -167,6 +176,54 @@ func classToVMMapperFn( } } +// encryptionClassToVMMapperFn returns a mapper function that can be used to +// enqueue reconcile requests for VMs in response to an event on the +// EncryptionClass resource. +func encryptionClassToVMMapperFn( + ctx *pkgctx.ControllerManagerContext, + c client.Client) func(_ context.Context, o client.Object) []reconcile.Request { + + // For a given EncryptionClass, return reconcile requests for VMs that + // specify the same EncryptionClass. + return func(_ context.Context, o client.Object) []reconcile.Request { + obj := o.(*byokv1.EncryptionClass) + logger := ctx.Logger.WithValues("name", obj.Name, "namespace", obj.Namespace) + + logger.V(4).Info("Reconciling all VMs referencing an EncryptionClass") + + // Find all VM resources that reference this EncryptionClass. + vmList := &vmopv1.VirtualMachineList{} + if err := c.List(ctx, vmList, client.InNamespace(obj.Namespace)); err != nil { + logger.Error( + err, + "Failed to list VirtualMachines for reconciliation due to EncryptionClass watch") + return nil + } + + // Populate reconcile requests for VMs that reference this + // EncryptionClass. + var requests []reconcile.Request + for _, vm := range vmList.Items { + if vm.Spec.Crypto.ClassName == obj.Name { + requests = append( + requests, + reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: vm.Namespace, + Name: vm.Name, + }, + }) + } + } + + logger.Info( + "Returning VM reconcile requests due to EncryptionClass watch", + "requests", requests) + + return requests + } +} + func upgradeSchema(ctx *pkgctx.VirtualMachineContext) { // If empty, this VM was created before v1alpha3 added the spec.instanceUUID field. if ctx.VM.Spec.InstanceUUID == "" && ctx.VM.Status.InstanceUUID != "" { diff --git a/go.mod b/go.mod index fc7b15596..9fe1d1121 100644 --- a/go.mod +++ b/go.mod @@ -30,9 +30,9 @@ require ( github.com/vmware-tanzu/vm-operator/external/storage-policy-quota v0.0.0-00010101000000-000000000000 github.com/vmware-tanzu/vm-operator/external/tanzu-topology v0.0.0-00010101000000-000000000000 github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels v0.0.0-00010101000000-000000000000 - github.com/vmware/govmomi v0.31.1-0.20240730173452-49b88eb9917f + github.com/vmware/govmomi v0.31.1-0.20240909180350-bc0c8a0855f7 // * https://github.com/vmware-tanzu/vm-operator/security/dependabot/24 - golang.org/x/text v0.16.0 + golang.org/x/text v0.18.0 k8s.io/api v0.31.0 k8s.io/apiextensions-apiserver v0.31.0 k8s.io/apimachinery v0.31.0 diff --git a/go.sum b/go.sum index 09ec928eb..33bc90537 100644 --- a/go.sum +++ b/go.sum @@ -459,8 +459,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-20240902045731-00a14868c72d h1:6pMXrQmTYpu5FipoQ9fT4FJG3VPMMbBoIi6h3KvdQc8= github.com/vmware-tanzu/nsx-operator/pkg/apis v0.0.0-20240902045731-00a14868c72d/go.mod h1:Q4JzNkNMvjo7pXtlB5/R3oME4Nhah7fAObWgghVmtxk= -github.com/vmware/govmomi v0.31.1-0.20240730173452-49b88eb9917f h1:lKzA28fIcNkcZgQdgP8YXaGZQcDMLNrwE9CyMyRKQNg= -github.com/vmware/govmomi v0.31.1-0.20240730173452-49b88eb9917f/go.mod h1:oHzAQ1r6152zYDGcUqeK+EO8LhKo5wjtvWZBGHws2Hc= +github.com/vmware/govmomi v0.31.1-0.20240909180350-bc0c8a0855f7 h1:HWzDs/a0bU5Mq95zFryr6lSPZobEC3hNiWaY/gRoBDE= +github.com/vmware/govmomi v0.31.1-0.20240909180350-bc0c8a0855f7/go.mod h1:IOv5nTXCPqH9qVJAlRuAGffogaLsNs8aF+e7vLgsHJU= 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/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -641,8 +641,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/providers/vsphere/constants/constants.go b/pkg/providers/vsphere/constants/constants.go index d989032b1..f4387282e 100644 --- a/pkg/providers/vsphere/constants/constants.go +++ b/pkg/providers/vsphere/constants/constants.go @@ -67,6 +67,10 @@ const ( CloudInitGuestInfoUserdata = "guestinfo.userdata" CloudInitGuestInfoUserdataEncoding = "guestinfo.userdata.encoding" + // CryptoIDAnnotation is the annotation key used to store the MD5 hash of + // the public provider and key IDs used to encrypt or recrypt a VM. + CryptoIDAnnotation = pkg.VMOperatorKey + "/crypto-provider-and-key-id-hash" + // InstanceStoragePVCNamePrefix prefix of auto-generated PVC names. InstanceStoragePVCNamePrefix = "instance-pvc-" // InstanceStorageLabelKey identifies resources related to instance storage. diff --git a/pkg/providers/vsphere/session/session_vm_update.go b/pkg/providers/vsphere/session/session_vm_update.go index 516cd9810..4d3cfe65f 100644 --- a/pkg/providers/vsphere/session/session_vm_update.go +++ b/pkg/providers/vsphere/session/session_vm_update.go @@ -16,6 +16,7 @@ import ( vimtypes "github.com/vmware/govmomi/vim25/types" apiEquality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" vmopv1common "github.com/vmware-tanzu/vm-operator/api/v1alpha3/common" @@ -29,6 +30,7 @@ import ( res "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/resources" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/virtualmachine" "github.com/vmware-tanzu/vm-operator/pkg/providers/vsphere/vmlifecycle" + "github.com/vmware-tanzu/vm-operator/pkg/reconfig" pkgutil "github.com/vmware-tanzu/vm-operator/pkg/util" "github.com/vmware-tanzu/vm-operator/pkg/util/annotations" "github.com/vmware-tanzu/vm-operator/pkg/util/resize" @@ -563,8 +565,10 @@ func (s *Session) prePowerOnVMReconfigure( vmCtx, vmCtx.Logger.WithName("prePowerOnVMReconfigure"), ), + s.K8sClient, vmCtx.VM, resVM.VcVM(), + vmCtx.MoVM, *configSpec); err != nil { return err @@ -805,8 +809,10 @@ func (s *Session) poweredOnVMReconfigure( vmCtx, vmCtx.Logger.WithName("poweredOnVMReconfigure"), ), + s.K8sClient, vmCtx.VM, resVM.VcVM(), + vmCtx.MoVM, *configSpec) if err != nil { @@ -897,8 +903,10 @@ func (s *Session) resizeVMWhenPoweredStateOff( vmCtx, vmCtx.Logger.WithName("resizeVMWhenPoweredStateOff"), ), + s.K8sClient, vmCtx.VM, vcVM, + vmCtx.MoVM, configSpec) if err != nil { @@ -1001,7 +1009,7 @@ func (s *Session) updateVMDesiredPowerStateOff( refetchProps = true } } else { - refetch, err := defaultReconfigure(vmCtx, vcVM, *vmCtx.MoVM.Config) + refetch, err := defaultReconfigure(vmCtx, s.K8sClient, vcVM) if err != nil { return refetchProps, err } @@ -1034,7 +1042,7 @@ func (s *Session) updateVMDesiredPowerStateSuspended( refetchProps = true } - refetch, err := defaultReconfigure(vmCtx, vcVM, *vmCtx.MoVM.Config) + refetch, err := defaultReconfigure(vmCtx, s.K8sClient, vcVM) if err != nil { return refetchProps, err } @@ -1208,7 +1216,7 @@ func (s *Session) UpdateVirtualMachine( } } else { vmCtx.Logger.Info("VirtualMachine is paused. PowerState is not updated.") - refetchProps, err = defaultReconfigure(vmCtx, vcVM, *vmCtx.MoVM.Config) + refetchProps, err = defaultReconfigure(vmCtx, s.K8sClient, vcVM) } if refetchProps { @@ -1258,14 +1266,14 @@ func isVMPaused(vmCtx pkgctx.VirtualMachineContext) bool { func defaultReconfigure( vmCtx pkgctx.VirtualMachineContext, - vcVM *object.VirtualMachine, - configInfo vimtypes.VirtualMachineConfigInfo) (bool, error) { + k8sClient ctrlclient.Client, + vcVM *object.VirtualMachine) (bool, error) { var configSpec vimtypes.VirtualMachineConfigSpec if err := vmopv1util.OverwriteAlwaysResizeConfigSpec( vmCtx, *vmCtx.VM, - configInfo, + *vmCtx.MoVM.Config, &configSpec); err != nil { return false, err @@ -1276,17 +1284,35 @@ func defaultReconfigure( vmCtx, vmCtx.Logger.WithName("defaultReconfigure"), ), + k8sClient, vmCtx.VM, vcVM, + vmCtx.MoVM, configSpec) } func doReconfigure( ctx context.Context, + k8sClient ctrlclient.Client, vm *vmopv1.VirtualMachine, vcVM *object.VirtualMachine, + moVM mo.VirtualMachine, configSpec vimtypes.VirtualMachineConfigSpec) (bool, error) { + if pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + for _, r := range reconfig.FromContext(ctx) { + if err := r.PreReconfigure( + ctx, + k8sClient, + vm, + moVM, + &configSpec); err != nil { + + return false, err + } + } + } + var defaultConfigSpec vimtypes.VirtualMachineConfigSpec if apiEquality.Semantic.DeepEqual(configSpec, defaultConfigSpec) { return false, nil @@ -1297,6 +1323,20 @@ func doReconfigure( UpdateVMGuestIDReconfiguredCondition(vm, configSpec, taskInfo) + if pkgcfg.FromContext(ctx).Features.BringYourOwnEncryptionKey { + for _, r := range reconfig.FromContext(ctx) { + if err := r.PostReconfigure( + ctx, + vm, + moVM, + configSpec, + err); err != nil { + + return false, err + } + } + } + if err != nil { return false, err } diff --git a/pkg/providers/vsphere/vmprovider_vm.go b/pkg/providers/vsphere/vmprovider_vm.go index dcd3689ee..f5738a3e2 100644 --- a/pkg/providers/vsphere/vmprovider_vm.go +++ b/pkg/providers/vsphere/vmprovider_vm.go @@ -515,7 +515,8 @@ func (vs *vSphereVMProvider) updateVirtualMachine( } getUpdateArgsFn := func() (*vmUpdateArgs, error) { - // TODO: Use createArgs if we already got them + // TODO: Use createArgs if we already got them, except for: + // - createArgs.ConfigSpec.Crypto _ = createArgs return vs.vmUpdateGetArgs(vmCtx) } @@ -1076,6 +1077,12 @@ func (vs *vSphereVMProvider) vmCreateGenConfigSpec( createArgs.ImageStatus, minCPUFreq) + // Get the encryption class details for the VM. + // TODO (akutz) + if pkgcfg.FromContext(vmCtx).Features.BringYourOwnEncryptionKey { + + } + err := vs.vmCreateGenConfigSpecExtraConfig(vmCtx, createArgs) if err != nil { return err diff --git a/pkg/providers/vsphere/vmprovider_vm_test.go b/pkg/providers/vsphere/vmprovider_vm_test.go index ab71d0e2f..55f89e3db 100644 --- a/pkg/providers/vsphere/vmprovider_vm_test.go +++ b/pkg/providers/vsphere/vmprovider_vm_test.go @@ -1359,6 +1359,108 @@ func vmTests() { }) }) + FContext("Encryption class", func() { + BeforeEach(func() { + vm.Spec.Crypto.ClassName = "my-encryption-class" + }) + + When("FSS_WCP_VMSERVICE_BYOK is disabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.BringYourOwnEncryptionKey = false + }) + }) + When("encryption class does not exist", func() { + It("should not return an error", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vm) + Expect(err).ToNot(HaveOccurred()) + Expect(vcVM).ToNot(BeNil()) + }) + }) + }) + + When("FSS_WCP_VMSERVICE_BYOK is enabled", func() { + JustBeforeEach(func() { + pkgcfg.SetContext(ctx, func(config *pkgcfg.Config) { + config.Features.BringYourOwnEncryptionKey = true + }) + }) + + useExistingVM := func(cryptoSpec vimtypes.BaseCryptoSpec) { + vmList, err := ctx.Finder.VirtualMachineList(ctx, "*") + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, vmList).ToNot(BeEmpty()) + + vcVM := vmList[0] + vm.Spec.BiosUUID = vcVM.UUID(ctx) + + powerState, err := vcVM.PowerState(ctx) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + if powerState == vimtypes.VirtualMachinePowerStatePoweredOn { + tsk, err := vcVM.PowerOff(ctx) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, tsk.Wait(ctx)).To(Succeed()) + } + + if cryptoSpec != nil { + tsk, err := vcVM.Reconfigure( + ctx, + vimtypes.VirtualMachineConfigSpec{ + Crypto: cryptoSpec, + }) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, tsk.Wait(ctx)).To(Succeed()) + } + } + + When("spec.encryptionClassName is empty", func() { + When("vm does not exist", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vm) + Expect(err).ToNot(HaveOccurred()) + Expect(vcVM).ToNot(BeNil()) + }) + When("vm does exist", func() { + When("vm is encrypted", func() { + JustBeforeEach(func() { + useExistingVM(&vimtypes.CryptoSpecEncrypt{ + CryptoKeyId: vimtypes.CryptoKeyId{ + ProviderId: &vimtypes.KeyProviderId{ + Id: "providerID", + }, + KeyId: "keyID", + }, + }) + }) + }) + When("vm is not encrypted", func() { + + }) + }) + }) + When("spec.encryptionClassName is not empty", func() { + When("encryption class does not exist", func() { + It("should return an error", func() { + vcVM, err := createOrUpdateAndGetVcVM(ctx, vm) + Expect(err).To(MatchError(`encryptionclasses.vmencryption.vmware.com "my-encryption-class" not found`)) + Expect(vcVM).ToNot(BeNil()) + }) + }) + When("encryption class does exist", func() { + When("vm does not exist", func() { + }) + When("vm does exist", func() { + When("vm is encrypted", func() { + + }) + When("vm is not encrypted", func() { + + }) + }) + }) + }) + }) + }) + Context("VM Class with PCI passthrough devices", func() { BeforeEach(func() { vmClass.Spec.Hardware.Devices = vmopv1.VirtualDevices{ diff --git a/pkg/reconfig/crypto/crypt_reconciler_suite_test.go b/pkg/reconfig/crypto/crypt_reconciler_suite_test.go new file mode 100644 index 000000000..6ff3e2565 --- /dev/null +++ b/pkg/reconfig/crypto/crypt_reconciler_suite_test.go @@ -0,0 +1,24 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/klog/v2" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +func init() { + klog.SetOutput(GinkgoWriter) + logf.SetLogger(klog.Background()) +} + +func TestSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Crypto Reconciler Test Suite") +} diff --git a/pkg/reconfig/crypto/crypto_reconciler.go b/pkg/reconfig/crypto/crypto_reconciler.go new file mode 100644 index 000000000..fad902c3c --- /dev/null +++ b/pkg/reconfig/crypto/crypto_reconciler.go @@ -0,0 +1,148 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto + +import ( + "fmt" + "strings" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + "github.com/vmware-tanzu/vm-operator/pkg/reconfig" +) + +type reconciler struct{} + +var _ reconfig.Reconciler = reconciler{} + +// New returns a new Reconciler for a VM's crypto state. +func New() reconfig.Reconciler { + return reconciler{} +} + +// Name returns the unique name used to identify the reconciler. +func (r reconciler) Name() string { + return "crypto" +} + +// SprintfStateNotSynced formats and returns the message for when the encryption +// state cannot be synced. +func SprintfStateNotSynced(op string, msgs ...string) string { + var msg string + switch len(msgs) { + case 1: + msg = msgs[0] + case 2: + msg = fmt.Sprintf("%s and %s", msgs[0], msgs[1]) + default: + msg = fmt.Sprintf( + "%s, and %s", + strings.Join(msgs[:len(msgs)-1], ", "), + msgs[len(msgs)-1]) + } + return fmt.Sprintf("Must %s when %s vm", msg, op) +} + +// ClassReadyReason is the type used by reasons given to the condition +// vmopv1.VirtualMachineEncryptionClassReady. +type ClassReadyReason uint + +const ( + ClassReadyReasonNotFound ClassReadyReason = 1 << iota + ClassReadyReasonNoProviderID + ClassReadyReasonNoKeyID +) + +func (r ClassReadyReason) Has(val ClassReadyReason) bool { + return enumHas(r, val) +} + +func (r *ClassReadyReason) Set(val ClassReadyReason) { + enumSet(r, val) +} + +func (r *ClassReadyReason) Unset(val ClassReadyReason) { + enumUnset(r, val) +} + +func (r ClassReadyReason) String() string { + switch { + case r == ClassReadyReasonNotFound: + return "NotFound" + case r == ClassReadyReasonNoProviderID: + return "NoProviderID" + case r == ClassReadyReasonNoKeyID: + return "NoKeyID" + case r.Has(ClassReadyReasonNoProviderID) && r.Has(ClassReadyReasonNoKeyID): + return "NoProviderOrKeyID" + } + return "" +} + +// StateSyncedReason is the type used by reasons given to the condition +// vmopv1.VirtualMachineEncryptionStateSynced. +type StateSyncedReason uint + +const ( + StateSyncedReasonInvalidState StateSyncedReason = 1 << iota + StateSyncedReasonInvalidChanges + StateSyncedReasonReconfigureError +) + +func (r StateSyncedReason) Has(val StateSyncedReason) bool { + return enumHas(r, val) +} + +func (r *StateSyncedReason) Set(val StateSyncedReason) { + enumSet(r, val) +} + +func (r *StateSyncedReason) Unset(val StateSyncedReason) { + enumUnset(r, val) +} + +func (r StateSyncedReason) String() string { + switch { + case r == StateSyncedReasonReconfigureError: + return "ReconfigureError" + case r == StateSyncedReasonInvalidState: + return "InvalidState" + case r == StateSyncedReasonInvalidChanges: + return "InvalidChanges" + case r.Has(StateSyncedReasonInvalidState) && r.Has(StateSyncedReasonInvalidChanges): + return "InvalidStateAndChanges" + } + return "" +} + +func enumHas[T ~uint](a, b T) bool { + return (a & b) > 0 +} + +func enumSet[T ~uint](a *T, b T) { + if !enumHas(*a, b) { + *a ^= b + } +} + +func enumUnset[T ~uint](a *T, b T) { + if enumHas(*a, b) { + *a &^= b + } +} + +func markEncryptionStateNotSynced( + vm *vmopv1.VirtualMachine, + op string, + reason StateSyncedReason, + msgs ...string) { + + fmt.Printf("op=%[1]s, reason=%[2]d;%[2]s, msgs=%[3]v\n", op, reason, msgs) + + conditions.MarkFalse( + vm, + vmopv1.VirtualMachineEncryptionSynced, + reason.String(), + SprintfStateNotSynced(op, msgs...)) +} diff --git a/pkg/reconfig/crypto/crypto_reconciler_post.go b/pkg/reconfig/crypto/crypto_reconciler_post.go new file mode 100644 index 000000000..8d26090c9 --- /dev/null +++ b/pkg/reconfig/crypto/crypto_reconciler_post.go @@ -0,0 +1,186 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto + +import ( + "context" + + "github.com/vmware/govmomi/fault" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" +) + +// PostReconfigure responds to the result of a Reconfigure operation. +// +//nolint:gocyclo +func (r reconciler) PostReconfigure( + ctx context.Context, + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine, + configSpec vimtypes.VirtualMachineConfigSpec, + reconfigErr error) error { + + if ctx == nil { + panic("context is nil") + } + if moVM.Config == nil { + panic("moVM.config is nil") + } + if vm == nil { + panic("vm is nil") + } + + if reconfigErr == nil { + + // If no reconfigure error occurred then we need to check if there was + // a crypto update as part of the reconfigure. + + if configSpec.Crypto != nil { + + // A crypto update was successful, so indicate that the encryption + // state of this VM is synced. + conditions.MarkTrue(vm, vmopv1.VirtualMachineEncryptionSynced) + } + + return nil + } + + // Determine the message to put on the condition. + var msgs []string + + // + // At this point we know that a reconfigure error occurred *and* there was + // a crypto update in the ConfigSpec. It is time to parse the reconfigErr + // to determine if it was related to the crypto update. + // + + fault.In( + reconfigErr, + func( + fault vimtypes.BaseMethodFault, + localizedMessage string, + localizableMessages []vimtypes.LocalizableMessage) bool { + + switch tErr := fault.(type) { + case *vimtypes.GenericVmConfigFault: + for i := range localizableMessages { + switch localizableMessages[i].Key { + case "msg.vigor.enc.keyNotFound": + msgs = append(msgs, "specify a valid key") + case "msg.keysafe.locator": + msgs = append(msgs, "specify a key that can be located") + case "msg.vtpm.add.notEncrypted": + msgs = append(msgs, "add vTPM") + case "msg.vigor.enc.required.vtpm": + msgs = append(msgs, "have vTPM") + } + } + case *vimtypes.SystemError: + switch localizedMessage { + case "Error creating disk Key locator": + msgs = append(msgs, "specify a valid key") + case "Key locator error": + msgs = append(msgs, "specify a key that can be located") + case "Key required for encryption.bundle.": + msgs = append(msgs, "not specify encryption bundle") + } + case *vimtypes.NotSupported: + for i := range localizableMessages { + //nolint:gocritic + switch localizableMessages[i].Key { + case "msg.disk.policyChangeFailure": + msgs = append(msgs, "not have encryption IO filter") + } + } + case *vimtypes.InvalidArgument: + for i := range localizableMessages { + //nolint:gocritic + switch localizableMessages[i].Key { + case "config.extraConfig[\"dataFileKey\"]": + msgs = append(msgs, "not set secret key") + } + } + case *vimtypes.InvalidDeviceOperation: + for i := range localizableMessages { + switch localizableMessages[i].Key { + case "msg.hostd.deviceSpec.enc.encrypted": + msgs = append(msgs, "not specify encrypted disk") + case "msg.hostd.deviceSpec.enc.notEncrypted": + msgs = append(msgs, "not specify decrypted disk") + default: + msgs = append(msgs, "not add/remove device sans crypto spec") + } + } + + case *vimtypes.InvalidDeviceSpec: + for i := range localizableMessages { + switch localizableMessages[i].Key { + case "msg.hostd.deviceSpec.enc.badPolicy": + msgs = append(msgs, "have encryption IO filter") + case "msg.hostd.deviceSpec.enc.notDisk": + msgs = append(msgs, "not apply only to disk") + case "msg.hostd.deviceSpec.enc.sharedBacking": + msgs = append(msgs, "not have disk with shared backing") + case "msg.hostd.deviceSpec.enc.notFile": + msgs = append(msgs, "not have raw disk mapping") + case "msg.hostd.configSpec.enc.mismatch": + msgs = append(msgs, "not add encrypted disk") + case "msg.hostd.deviceSpec.add.noencrypt": + msgs = append(msgs, "not add plain disk") + } + } + case *vimtypes.InvalidPowerState: + if tErr.ExistingState != vimtypes.VirtualMachinePowerStatePoweredOff { + msgs = append(msgs, "be powered off") + } + case *vimtypes.InvalidVmConfig: + for i := range localizableMessages { + switch localizableMessages[i].Key { + case "msg.hostd.configSpec.enc.snapshots": + msgs = append(msgs, "not have snapshots") + case "msg.hostd.deviceSpec.enc.diskChain": + msgs = append(msgs, "not have only disk snapshots") + case "msg.hostd.configSpec.enc.notEncrypted": + msgs = append(msgs, "not be encrypted") + case "msg.hostd.configSpec.enc.encrypted": + msgs = append(msgs, "be encrypted") + case "msg.hostd.configSpec.enc.mismatch": + msgs = append(msgs, "have vm and disks with different encryption states") + } + } + } + return false + }, + ) + + if len(msgs) > 0 { + var op string + switch configSpec.Crypto.(type) { + case *vimtypes.CryptoSpecDecrypt: + op = "decrypting" + case *vimtypes.CryptoSpecEncrypt: + op = "encrypting" + case *vimtypes.CryptoSpecShallowRecrypt: + op = "recrypting" + default: + if moVM.Config.KeyId != nil { + op = "updating encrypted" + } else { + op = "updating unencrypted" + } + } + + markEncryptionStateNotSynced( + vm, + op, + StateSyncedReasonReconfigureError, + msgs...) + } + + return nil +} diff --git a/pkg/reconfig/crypto/crypto_reconciler_post_test.go b/pkg/reconfig/crypto/crypto_reconciler_post_test.go new file mode 100644 index 000000000..e76a1aa96 --- /dev/null +++ b/pkg/reconfig/crypto/crypto_reconciler_post_test.go @@ -0,0 +1,565 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto_test + +import ( + "bytes" + "context" + "fmt" + "reflect" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vmware/govmomi/task" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/soap" + vimtypes "github.com/vmware/govmomi/vim25/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + "github.com/vmware-tanzu/vm-operator/pkg/reconfig" + "github.com/vmware-tanzu/vm-operator/pkg/reconfig/crypto" +) + +var _ = Describe("PostReconfigure", func() { + var ( + r reconfig.Reconciler + ctx context.Context + moVM mo.VirtualMachine + vm *vmopv1.VirtualMachine + configSpec vimtypes.VirtualMachineConfigSpec + reconfigErr error + ) + + BeforeEach(func() { + r = crypto.New() + ctx = context.Background() + moVM.Config = &vimtypes.VirtualMachineConfigInfo{} + vm = &vmopv1.VirtualMachine{} + configSpec = vimtypes.VirtualMachineConfigSpec{} + reconfigErr = nil + }) + + assertStateNotSynced := func( + vm *vmopv1.VirtualMachine, + expectedMessage string) { + + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + ExpectWithOffset(1, c).ToNot(BeNil()) + ExpectWithOffset(1, c.Status).To(Equal(metav1.ConditionFalse)) + ExpectWithOffset(1, c.Reason).To(Equal(crypto.StateSyncedReasonReconfigureError.String())) + ExpectWithOffset(1, c.Message).To(Equal(expectedMessage)) + } + + When("ctx is nil", func() { + BeforeEach(func() { + ctx = nil + }) + It("should panic", func() { + fn := func() { + _ = r.PostReconfigure(ctx, vm, moVM, configSpec, reconfigErr) + } + Expect(fn).To(PanicWith("context is nil")) + }) + }) + + When("moVM.config is nil", func() { + BeforeEach(func() { + moVM.Config = nil + }) + It("should panic", func() { + fn := func() { + _ = r.PostReconfigure(ctx, vm, moVM, configSpec, reconfigErr) + } + Expect(fn).To(PanicWith("moVM.config is nil")) + }) + }) + + When("vm is nil", func() { + BeforeEach(func() { + vm = nil + }) + It("should panic", func() { + fn := func() { + _ = r.PostReconfigure(ctx, vm, moVM, configSpec, reconfigErr) + } + Expect(fn).To(PanicWith("vm is nil")) + }) + }) + + When("reconfig", func() { + var ( + err error + ) + JustBeforeEach(func() { + err = r.PostReconfigure(ctx, vm, moVM, configSpec, reconfigErr) + }) + + Context("succeeded", func() { + When("crypto was not involved", func() { + BeforeEach(func() { + configSpec.Crypto = nil + }) + When("vm does not already have crypto synced condition", func() { + BeforeEach(func() { + conditions.Delete(vm, vmopv1.VirtualMachineEncryptionSynced) + }) + It("should leave the condition alone", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeFalse()) + }) + }) + When("vm does already have crypto synced condition", func() { + When("existing condition is true", func() { + BeforeEach(func() { + conditions.MarkTrue(vm, vmopv1.VirtualMachineEncryptionSynced) + }) + It("should leave the condition alone", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + When("existing condition is false", func() { + BeforeEach(func() { + conditions.MarkFalse(vm, vmopv1.VirtualMachineEncryptionSynced, "fake", "fake") + }) + It("should leave the condition alone", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsFalse(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + }) + When("crypto was involved", func() { + BeforeEach(func() { + configSpec.Crypto = &vimtypes.CryptoSpecNoOp{} + }) + When("vm does not already have crypto synced condition", func() { + BeforeEach(func() { + conditions.Delete(vm, vmopv1.VirtualMachineEncryptionSynced) + }) + It("should set the condition to true", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + When("vm does already have crypto synced condition", func() { + When("existing condition is true", func() { + BeforeEach(func() { + conditions.MarkTrue(vm, vmopv1.VirtualMachineEncryptionSynced) + }) + It("should set the condition to true", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + When("existing condition is false", func() { + BeforeEach(func() { + conditions.MarkFalse(vm, vmopv1.VirtualMachineEncryptionSynced, "fake", "fake") + }) + It("should set the condition to true", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeTrue()) + }) + }) + }) + }) + }) + + When("failed", func() { + Context("with a soap error", func() { + BeforeEach(func() { + reconfigErr = soap.WrapSoapFault(&soap.Fault{ + Detail: struct { + Fault vimtypes.AnyType "xml:\",any,typeattr\"" + }{ + Fault: nil, + }, + }) + }) + It("should not return an error or set any conditions", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeFalse()) + }) + }) + + Context("with a task error", func() { + Context("and is a nil LocalizedMethodFault", func() { + BeforeEach(func() { + reconfigErr = task.Error{} + }) + It("should not return an error or set any conditions", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeFalse()) + }) + }) + + Context("and is an unknown fault", func() { + BeforeEach(func() { + reconfigErr = task.Error{ + LocalizedMethodFault: &vimtypes.LocalizedMethodFault{}, + } + }) + It("should not return an error or set any conditions", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Has(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeFalse()) + }) + }) + + DescribeTable("and is a single, known fault", + func( + currentCryptoState *vimtypes.CryptoKeyId, + pendingCryptoState vimtypes.BaseCryptoSpec, + fault vimtypes.BaseMethodFault, + expectedConditionMessage, + localizedMessage string, + msgKeys []string) { + + vm := &vmopv1.VirtualMachine{} + + mf := fault.GetMethodFault() + for i := range msgKeys { + mf.FaultMessage = append( + mf.FaultMessage, + vimtypes.LocalizableMessage{ + Key: msgKeys[i], + }) + } + + Expect(r.PostReconfigure( + ctx, + vm, + mo.VirtualMachine{ + Config: &vimtypes.VirtualMachineConfigInfo{ + KeyId: currentCryptoState, + }, + }, + vimtypes.VirtualMachineConfigSpec{ + Crypto: pendingCryptoState, + }, + task.Error{ + LocalizedMethodFault: &vimtypes.LocalizedMethodFault{ + Fault: fault, + LocalizedMessage: localizedMessage, + }, + })).To(Succeed()) + + assertStateNotSynced(vm, expectedConditionMessage) + }, + + joinTableEntries( + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.GenericVmConfigFault{} }, + "specify a valid key", + "", + "msg.vigor.enc.keyNotFound", + ), + + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.GenericVmConfigFault{} }, + "specify a key that can be located", + "", + "msg.keysafe.locator", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.GenericVmConfigFault{} }, + "add vTPM", + "", + "msg.vtpm.add.notEncrypted", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.GenericVmConfigFault{} }, + "have vTPM", + "", + "msg.vigor.enc.required.vtpm", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.GenericVmConfigFault{} }, + "specify a valid key and specify a key that can be located", + "", + "msg.vigor.enc.keyNotFound", + "msg.keysafe.locator", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.GenericVmConfigFault{} }, + "specify a valid key, specify a key that can be located, and add vTPM", + "", + "msg.vigor.enc.keyNotFound", + "msg.keysafe.locator", + "msg.vtpm.add.notEncrypted", + ), + + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.SystemError{} }, + "specify a valid key", + "Error creating disk Key locator", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.SystemError{} }, + "specify a key that can be located", + "Key locator error", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.SystemError{} }, + "not specify encryption bundle", + "Key required for encryption.bundle.", + ), + + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.NotSupported{} }, + "not have encryption IO filter", + "", + "msg.disk.policyChangeFailure", + ), + + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceOperation{} }, + "not specify encrypted disk", + "", + "msg.hostd.deviceSpec.enc.encrypted", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceOperation{} }, + "not specify decrypted disk", + "", + "msg.hostd.deviceSpec.enc.notEncrypted", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceOperation{} }, + "not add/remove device sans crypto spec", + "", + "fake.msg.id", + ), + + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidArgument{} }, + "not set secret key", + "", + "config.extraConfig[\"dataFileKey\"]", + ), + + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceSpec{} }, + "have encryption IO filter", + "", + "msg.hostd.deviceSpec.enc.badPolicy", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceSpec{} }, + "not apply only to disk", + "", + "msg.hostd.deviceSpec.enc.notDisk", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceSpec{} }, + "not have disk with shared backing", + "", + "msg.hostd.deviceSpec.enc.sharedBacking", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceSpec{} }, + "not have raw disk mapping", + "", + "msg.hostd.deviceSpec.enc.notFile", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceSpec{} }, + "not add encrypted disk", + "", + "msg.hostd.configSpec.enc.mismatch", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidDeviceSpec{} }, + "not add plain disk", + "", + "msg.hostd.deviceSpec.add.noencrypt", + ), + + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidVmConfig{} }, + "not have snapshots", + "", + "msg.hostd.configSpec.enc.snapshots", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidVmConfig{} }, + "not have only disk snapshots", + "", + "msg.hostd.deviceSpec.enc.diskChain", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidVmConfig{} }, + "not be encrypted", + "", + "msg.hostd.configSpec.enc.notEncrypted", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidVmConfig{} }, + "be encrypted", + "", + "msg.hostd.configSpec.enc.encrypted", + ), + getMethodFaultTableEntries( + func() vimtypes.BaseMethodFault { return &vimtypes.InvalidVmConfig{} }, + "have vm and disks with different encryption states", + "", + "msg.hostd.configSpec.enc.mismatch", + ), + ), + ) + + Context("and is multiple, known faults", func() { + When("there are two faults", func() { + BeforeEach(func() { + reconfigErr = task.Error{ + LocalizedMethodFault: &vimtypes.LocalizedMethodFault{ + LocalizedMessage: "Key locator error", + Fault: &vimtypes.SystemError{ + RuntimeFault: vimtypes.RuntimeFault{ + MethodFault: vimtypes.MethodFault{ + FaultCause: &vimtypes.LocalizedMethodFault{ + Fault: &vimtypes.NotSupported{ + RuntimeFault: vimtypes.RuntimeFault{ + MethodFault: vimtypes.MethodFault{ + FaultMessage: []vimtypes.LocalizableMessage{ + { + Key: "msg.disk.policyChangeFailure", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + }) + It("should return nil error and set a condition with the expected message", func() { + Expect(err).ToNot(HaveOccurred()) + assertStateNotSynced(vm, "Must specify a key that can be located and not have encryption IO filter when updating unencrypted vm") + }) + }) + When("there are three faults", func() { + BeforeEach(func() { + reconfigErr = task.Error{ + LocalizedMethodFault: &vimtypes.LocalizedMethodFault{ + LocalizedMessage: "Key locator error", + Fault: &vimtypes.SystemError{ + RuntimeFault: vimtypes.RuntimeFault{ + MethodFault: vimtypes.MethodFault{ + FaultCause: &vimtypes.LocalizedMethodFault{ + Fault: &vimtypes.NotSupported{ + RuntimeFault: vimtypes.RuntimeFault{ + MethodFault: vimtypes.MethodFault{ + FaultMessage: []vimtypes.LocalizableMessage{ + { + Key: "msg.disk.policyChangeFailure", + }, + }, + FaultCause: &vimtypes.LocalizedMethodFault{ + Fault: &vimtypes.InvalidPowerState{}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + }) + It("should return nil error and set a condition with the expected message", func() { + Expect(err).ToNot(HaveOccurred()) + assertStateNotSynced(vm, "Must specify a key that can be located, not have encryption IO filter, and be powered off when updating unencrypted vm") + }) + }) + }) + }) + }) + }) +}) + +func joinTableEntries(entries ...[]TableEntry) []TableEntry { + var list []TableEntry + for i := range entries { + list = append(list, entries[i]...) + } + return list +} + +func getMethodFaultTableEntries( + newFault func() vimtypes.BaseMethodFault, + conditionMessage, + localizedMessage string, + msgKeys ...string) []TableEntry { + + faultName := reflect.ValueOf(newFault()).Elem().Type().Name() + + var w bytes.Buffer + if localizedMessage != "" { + fmt.Fprintf(&w, "localizedMessage=%s", localizedMessage) + } + + if len(msgKeys) > 0 { + if w.Len() > 0 { + w.WriteString(", ") + } + fmt.Fprintf(&w, "messageKeys=%s", strings.Join(msgKeys, ";")) + } + + titleSuffix := w.String() + + return []TableEntry{ + Entry( + fmt.Sprintf("%s, decrypting vm, %s", faultName, titleSuffix), + &vimtypes.CryptoKeyId{}, + &vimtypes.CryptoSpecDecrypt{}, + newFault(), + crypto.SprintfStateNotSynced("decrypting", conditionMessage), + localizedMessage, + msgKeys, + ), + Entry( + fmt.Sprintf("%s, encrypting vm, %s", faultName, titleSuffix), + nil, + &vimtypes.CryptoSpecEncrypt{}, + newFault(), + crypto.SprintfStateNotSynced("encrypting", conditionMessage), + localizedMessage, + msgKeys, + ), + Entry( + fmt.Sprintf("%s, recrypting vm, %s", faultName, titleSuffix), + &vimtypes.CryptoKeyId{}, + &vimtypes.CryptoSpecShallowRecrypt{}, + newFault(), + crypto.SprintfStateNotSynced("recrypting", conditionMessage), + localizedMessage, + msgKeys, + ), + Entry( + fmt.Sprintf("%s, updating encrypted vm, %s", faultName, titleSuffix), + &vimtypes.CryptoKeyId{}, + nil, + newFault(), + crypto.SprintfStateNotSynced("updating encrypted", conditionMessage), + localizedMessage, + msgKeys, + ), + Entry( + fmt.Sprintf("%s, updating unencrypted vm, %s", faultName, titleSuffix), + nil, + nil, + newFault(), + crypto.SprintfStateNotSynced("updating unencrypted", conditionMessage), + localizedMessage, + msgKeys, + ), + } +} diff --git a/pkg/reconfig/crypto/crypto_reconciler_pre.go b/pkg/reconfig/crypto/crypto_reconciler_pre.go new file mode 100644 index 000000000..7545c92e0 --- /dev/null +++ b/pkg/reconfig/crypto/crypto_reconciler_pre.go @@ -0,0 +1,680 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto + +import ( + "context" + "regexp" + "strings" + + "github.com/go-logr/logr" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + byokv1 "github.com/vmware-tanzu/vm-operator/external/byok/api/v1alpha1" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" +) + +type ptrCfgSpec = *vimtypes.VirtualMachineConfigSpec + +// PreReconfigure attempts to update the provided ConfigSpec so a Reconfigure +// operation will reconcile the difference between the current and desired +// states managed by the reconciler. +func (r reconciler) PreReconfigure( + ctx context.Context, + k8sClient ctrlclient.Client, + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine, + configSpec ptrCfgSpec) error { + + if ctx == nil { + panic("context is nil") + } + if k8sClient == nil { + panic("k8sClient is nil") + } + if vm == nil { + panic("vm is nil") + } + if moVM.Config == nil { + panic("moVM.config is nil") + } + if configSpec == nil { + panic("configSpec is nil") + } + + var ( + newProID string + newKeyID string + curProID string + curKeyID string + ) + + if objName := vm.Spec.Crypto.ClassName; objName == "" { + + // When no encryption class is specified, remove any condition related + // to the encryption class. + conditions.Delete(vm, vmopv1.VirtualMachineEncryptionClassReady) + + } else { + + // When an encryption class is specified, get the provider ID and key ID + // from the encryption class. + objKey := ctrlclient.ObjectKey{ + Namespace: vm.Namespace, + Name: objName, + } + + var obj byokv1.EncryptionClass + if err := k8sClient.Get(ctx, objKey, &obj); err != nil { + if !apierrors.IsNotFound(err) { + return err + } + + conditions.MarkFalse( + vm, + vmopv1.VirtualMachineEncryptionClassReady, + ClassReadyReasonNotFound.String(), + "") + + // Allow the other reconciliation logic to continue. + // The VM's encryption state can be synced once the encryption + // class is ready. + return nil + + } + + newKeyID, newProID = obj.Spec.KeyID, obj.Spec.KeyProvider + + if newProID == "" && newKeyID != "" { + conditions.MarkTrue( + vm, + vmopv1.VirtualMachineEncryptionClassReady) + } else { + var reason ClassReadyReason + if newProID == "" { + reason.Set(ClassReadyReasonNoProviderID) + } + if newKeyID == "" { + reason.Set(ClassReadyReasonNoKeyID) + } + conditions.MarkFalse( + vm, + vmopv1.VirtualMachineEncryptionClassReady, + reason.String(), + "") + + // Allow the other reconciliation logic to continue. + // The VM's encryption state can be synced once the encryption + // class is ready. + return nil + } + } + + // Check to see if the VM is currently encrypted and record the current + // provider ID and key ID. + if kid := moVM.Config.KeyId; kid != nil { + curKeyID = kid.KeyId + if pid := kid.ProviderId; pid != nil { + curProID = pid.Id + } + } + + // TODO (akutz) even if newProId and newKeyId are empty, if VM is using + // encryption storage class, and there is a default key + // provider, I need to treat vm as it will be encrypted, + // possibly requesting a new key ID from from the default KMS + + var ( + op string + msgs []string + reason StateSyncedReason + ) + + switch { + case curKeyID != "" && newKeyID == "": + // TODO (akutz) Not decrypt, but leave alone or recrypt to default KMS + op = "decrypting" + reason, msgs = onDecrypt(ctx, moVM, configSpec, curProID, curKeyID) + + case curKeyID == "" && newKeyID != "": + op = "encrypting" + reason, msgs = onEncrypt(ctx, moVM, configSpec, newProID, newKeyID) + + case curProID+curKeyID != newProID+newKeyID: + op = "recrypting" + reason, msgs = onRecrypt( + ctx, moVM, configSpec, + curProID, curKeyID, + newProID, newKeyID) + + case moVM.Config.KeyId != nil: + op = "updating encrypted" + reason, msgs = validateUpdateEncrypted(moVM, configSpec) + + case moVM.Config.KeyId == nil: + op = "updating unencrypted" + reason, msgs = validateUpdateUnencrypted(moVM, configSpec) + } + + if len(msgs) > 0 { + markEncryptionStateNotSynced(vm, op, reason, msgs...) + } + + return nil +} + +func onDecrypt( + ctx context.Context, + moVM mo.VirtualMachine, + configSpec ptrCfgSpec, + curProID, curKeyID string) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + logger = logr.FromContextOrDiscard(ctx) + ) + + if reason, msgs = validateDecrypt( + moVM, + configSpec); len(msgs) > 0 { + + return reason, msgs + } + + configSpec.Crypto = &vimtypes.CryptoSpecDecrypt{} + + logger.Info( + "Decrypt VM", + "currentProviderID", curProID, + "currentKeyID", curKeyID) + + return 0, nil +} + +func onEncrypt( + ctx context.Context, + moVM mo.VirtualMachine, + configSpec ptrCfgSpec, + newProID, newKeyID string) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + logger = logr.FromContextOrDiscard(ctx) + ) + + if reason, msgs = validateEncrypt( + moVM, + configSpec, + newProID, newKeyID); len(msgs) > 0 { + + return reason, msgs + } + + var pid *vimtypes.KeyProviderId + if newProID != "" { + pid = &vimtypes.KeyProviderId{ + Id: newProID, + } + } + + configSpec.Crypto = &vimtypes.CryptoSpecEncrypt{ + CryptoKeyId: vimtypes.CryptoKeyId{ + ProviderId: pid, + KeyId: newKeyID, + }, + } + + logger.Info( + "Encrypt VM", + "newProviderID", newProID, + "newKeyID", newKeyID) + + return 0, nil +} + +func onRecrypt( + ctx context.Context, + moVM mo.VirtualMachine, + configSpec ptrCfgSpec, + curProID, curKeyID, + newProID, newKeyID string) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + logger = logr.FromContextOrDiscard(ctx) + ) + + if reason, msgs = validateRecrypt( + moVM, + configSpec, + newProID, newKeyID); len(msgs) > 0 { + + return reason, msgs + } + + var pid *vimtypes.KeyProviderId + if newProID != "" { + pid = &vimtypes.KeyProviderId{ + Id: newProID, + } + } + + configSpec.Crypto = &vimtypes.CryptoSpecShallowRecrypt{ + NewKeyId: vimtypes.CryptoKeyId{ + ProviderId: pid, + KeyId: newKeyID, + }, + } + + logger.Info( + "Recrypt VM", + "currentProviderID", curProID, + "currentKeyID", curKeyID, + "newProviderID", newProID, + "newKeyID", newKeyID) + + return 0, nil +} + +func validateEncrypt( + moVM mo.VirtualMachine, + configSpec ptrCfgSpec, + _, _ string) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + ) + + if r, m := validatePoweredOffNoSnapshots(moVM, configSpec); len(m) > 0 { + reason.Set(r) + msgs = append(msgs, m...) + } + if r, m := validateDeviceChanges(moVM, configSpec); len(m) > 0 { + reason.Set(r) + msgs = append(msgs, m...) + } + + return reason, msgs +} + +func validateDecrypt( + moVM mo.VirtualMachine, + configSpec ptrCfgSpec) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + ) + + if r, m := validatePoweredOffNoSnapshots(moVM, configSpec); len(m) > 0 { + reason.Set(r) + msgs = append(msgs, m...) + } + for i := range moVM.Config.Hardware.Device { + if _, ok := moVM.Config.Hardware.Device[i].(*vimtypes.VirtualTPM); ok { + msgs = append(msgs, "not have vTPM") + reason.Set(StateSyncedReasonInvalidState) + break + } + } + + return reason, msgs +} + +func validateRecrypt( + moVM mo.VirtualMachine, + configSpec ptrCfgSpec, + _, _ string) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + ) + + if hasSnapshotTree(moVM) { + msgs = append(msgs, "not have snapshot tree") + reason.Set(StateSyncedReasonInvalidState) + } + if r, m := validateDeviceChanges(moVM, configSpec); len(m) > 0 { + reason.Set(r) + msgs = append(msgs, m...) + } + + return reason, msgs +} + +func validateUpdateEncrypted( + moVM mo.VirtualMachine, + configSpec ptrCfgSpec) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + ) + + if isChangingSecretKey(moVM, configSpec) { + msgs = append(msgs, "not add/remove/modify secret key") + reason.Set(StateSyncedReasonInvalidChanges) + } + if r, m := validateDeviceChanges(moVM, configSpec); len(m) > 0 { + reason.Set(r) + msgs = append(msgs, m...) + } + + return reason, msgs +} + +func validateUpdateUnencrypted( + _ mo.VirtualMachine, + configSpec ptrCfgSpec) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + ) + + for i := range configSpec.DeviceChange { + devChange := configSpec.DeviceChange[i] + if dc := devChange.GetVirtualDeviceConfigSpec(); dc != nil { + //nolint:gocritic + switch dc.Device.(type) { + case *vimtypes.VirtualTPM: + if dc.Operation == vimtypes.VirtualDeviceConfigSpecOperationAdd { + msgs = append(msgs, "not add vTPM") + reason.Set(StateSyncedReasonInvalidChanges) + } + } + } + } + + return reason, msgs +} + +func validatePoweredOffNoSnapshots( + moVM mo.VirtualMachine, + _ ptrCfgSpec) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + ) + + if moVM.Summary.Runtime.PowerState != vimtypes.VirtualMachinePowerStatePoweredOff { + msgs = append(msgs, "be powered off") + reason.Set(StateSyncedReasonInvalidState) + } + if moVM.Snapshot != nil && moVM.Snapshot.CurrentSnapshot != nil { + msgs = append(msgs, "not have snapshots") + reason.Set(StateSyncedReasonInvalidState) + } + + return reason, msgs +} + +func validateDeviceChanges( + _ mo.VirtualMachine, + configSpec ptrCfgSpec) (StateSyncedReason, []string) { + + var ( + msgs []string + reason StateSyncedReason + ) + + for i := range configSpec.DeviceChange { + if devChange := configSpec.DeviceChange[i]; devChange != nil { + devSpec := devChange.GetVirtualDeviceConfigSpec() + if isAddEditDeviceSpecEncryptedSansPolicy(devSpec) { + msgs = append(msgs, "specify policy when encrypting devices") + reason.Set(StateSyncedReasonInvalidChanges) + } + if isEncryptedRawDiskMapping(devSpec) { + msgs = append(msgs, "not encrypt raw disks") + reason.Set(StateSyncedReasonInvalidChanges) + } + if isEncryptedDeviceNonDisk(devSpec) { + msgs = append(msgs, "not encrypt non-disk devices") + reason.Set(StateSyncedReasonInvalidChanges) + } + if isEncryptedDeviceWithMultipleBackings(devSpec) { + msgs = append(msgs, "not encrypt devices with multiple backings") + reason.Set(StateSyncedReasonInvalidChanges) + } + } + } + + return reason, msgs +} + +var secretKeys = map[string]struct{}{ + "ancestordatafilekeys": {}, + "cryptostate": {}, + "datafilekey": {}, + "encryption.required": {}, + "encryption.required.vtpm": {}, + "encryption.unspecified.default": {}, +} + +func isChangingSecretKey( + _ mo.VirtualMachine, + configSpec ptrCfgSpec) bool { + + for i := range configSpec.ExtraConfig { + if bov := configSpec.ExtraConfig[i]; bov != nil { + if ov := bov.GetOptionValue(); ov != nil { + if _, ok := secretKeys[ov.Key]; ok { + return true + } + } + } + } + return false +} + +func isAddEditDeviceSpecEncryptedSansPolicy( + spec *vimtypes.VirtualDeviceConfigSpec) bool { + + if spec != nil { + switch spec.Operation { + case vimtypes.VirtualDeviceConfigSpecOperationAdd, + vimtypes.VirtualDeviceConfigSpecOperationEdit: + + if backing := spec.Backing; backing != nil { + switch backing.Crypto.(type) { + case *vimtypes.CryptoSpecEncrypt, + *vimtypes.CryptoSpecDeepRecrypt, + *vimtypes.CryptoSpecShallowRecrypt: + + for i := range spec.Profile { + if doesProfileHaveIOFilters(spec.Profile[i]) { + return false + } + } + return true // is encrypted/recrypted sans policy + } + } + } + } + return false +} + +var whiteSpaceRegex = regexp.MustCompile(`[\s\t\n\r]`) + +func doesProfileHaveIOFilters(spec vimtypes.BaseVirtualMachineProfileSpec) bool { + if profile, ok := spec.(*vimtypes.VirtualMachineDefinedProfileSpec); ok { + if data := profile.ProfileData; data != nil { + if data.ExtensionKey == "com.vmware.vim.sips" { + return strings.Contains( + whiteSpaceRegex.ReplaceAllString(data.ObjectData, ""), + "IOFILTERS") + } + } + } + return false +} + +func isEncryptedDeviceNonDisk( + spec *vimtypes.VirtualDeviceConfigSpec) bool { + + if spec != nil { + if backing := spec.Backing; backing != nil { + switch backing.Crypto.(type) { + case *vimtypes.CryptoSpecEncrypt, + *vimtypes.CryptoSpecDeepRecrypt, + *vimtypes.CryptoSpecShallowRecrypt: + + _, isDisk := spec.Device.(*vimtypes.VirtualDisk) + if !isDisk { + return true + } + } + } + } + return false +} + +func isEncryptedDeviceWithMultipleBackings( + spec *vimtypes.VirtualDeviceConfigSpec) bool { + + if spec != nil { + if backing := spec.Backing; backing != nil { + switch backing.Crypto.(type) { + case *vimtypes.CryptoSpecEncrypt, + *vimtypes.CryptoSpecDeepRecrypt, + *vimtypes.CryptoSpecShallowRecrypt: + + return spec.Backing.Parent != nil + } + } + } + return false +} + +func isEncryptedRawDiskMapping( + spec *vimtypes.VirtualDeviceConfigSpec) bool { + + if spec != nil { + if backing := spec.Backing; backing != nil { + switch backing.Crypto.(type) { + case *vimtypes.CryptoSpecEncrypt, + *vimtypes.CryptoSpecDeepRecrypt, + *vimtypes.CryptoSpecShallowRecrypt: + + if disk, ok := spec.Device.(*vimtypes.VirtualDisk); ok { + //nolint:gocritic + switch disk.Backing.(type) { + case *vimtypes.VirtualDiskRawDiskVer2BackingInfo: + return true + } + } + } + } + } + return false +} + +// not supported. +// func isInvalidProviderOrKeyID(providerID, keyID string) []string { + +// return nil +// } + +// not supported. +// func hasNewDisksWithCryptoSpecNotEncrypt( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +// not supported. +// func isDecryptingNotEncryptedDisks( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +// not supported. +// func isRecryptingNotEncryptedDisks( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +// not supported. +// func isSettingEncryptionBundleWhileNotEncrypted( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +// not supported. +// func hasEncryptedDiskWithSharedBacking( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +// not supported. +// func isAddingEncryptedDiskWhileDecryptingVM( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +// not supported. +// func isEncryptingDiskWhileDecryptingVM( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +// not supported. +// func isEncrypingDiskWhileAddingItToVM( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +// not supported. +// func isDecryptingDiskWhileAddingItToVM( +// moVM mo.VirtualMachine, +// configSpec ptrCfgSpec) []string { + +// return nil +// } + +func hasSnapshotTree(moVM mo.VirtualMachine) bool { + var snapshotTree []vimtypes.VirtualMachineSnapshotTree + if si := moVM.Snapshot; si != nil { + snapshotTree = si.RootSnapshotList + } + return hasSnapshotTreeInner(snapshotTree) +} + +func hasSnapshotTreeInner(nodes []vimtypes.VirtualMachineSnapshotTree) bool { + switch len(nodes) { + case 0: + return false + case 1: + return hasSnapshotTreeInner(nodes[0].ChildSnapshotList) + default: + return true + } +} diff --git a/pkg/reconfig/crypto/crypto_reconciler_pre_test.go b/pkg/reconfig/crypto/crypto_reconciler_pre_test.go new file mode 100644 index 000000000..50296c057 --- /dev/null +++ b/pkg/reconfig/crypto/crypto_reconciler_pre_test.go @@ -0,0 +1,678 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto_test + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + byokv1 "github.com/vmware-tanzu/vm-operator/external/byok/api/v1alpha1" + "github.com/vmware-tanzu/vm-operator/pkg/conditions" + "github.com/vmware-tanzu/vm-operator/pkg/reconfig" + "github.com/vmware-tanzu/vm-operator/pkg/reconfig/crypto" + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +var _ = Describe("PreReconfigure", func() { + var ( + r reconfig.Reconciler + ctx context.Context + k8sClient ctrlclient.Client + moVM mo.VirtualMachine + vm *vmopv1.VirtualMachine + encClass *byokv1.EncryptionClass + withObjects []ctrlclient.Object + configSpec *vimtypes.VirtualMachineConfigSpec + ) + + BeforeEach(func() { + r = crypto.New() + ctx = context.Background() + moVM = mo.VirtualMachine{ + Config: &vimtypes.VirtualMachineConfigInfo{ + KeyId: &vimtypes.CryptoKeyId{ + KeyId: "my-key-id", + ProviderId: &vimtypes.KeyProviderId{ + Id: "my-provider-id", + }, + }, + }, + Summary: vimtypes.VirtualMachineSummary{ + Runtime: vimtypes.VirtualMachineRuntimeInfo{ + PowerState: vimtypes.VirtualMachinePowerStatePoweredOff, + }, + }, + } + vm = &vmopv1.VirtualMachine{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + Name: "my-vm", + }, + Spec: vmopv1.VirtualMachineSpec{ + Crypto: vmopv1.VirtualMachineCryptoSpec{ + ClassName: "my-encryption-class", + }, + }, + } + encClass = &byokv1.EncryptionClass{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + Name: "my-encryption-class", + }, + Spec: byokv1.EncryptionClassSpec{ + KeyProvider: "my-provider-id", + KeyID: "my-key-id", + }, + } + configSpec = &vimtypes.VirtualMachineConfigSpec{} + withObjects = []ctrlclient.Object{vm, encClass} + }) + JustBeforeEach(func() { + k8sClient = builder.NewFakeClient(withObjects...) + }) + + When("ctx is nil", func() { + BeforeEach(func() { + ctx = nil + }) + It("should panic", func() { + fn := func() { + _ = r.PreReconfigure(ctx, k8sClient, vm, moVM, configSpec) + } + Expect(fn).To(PanicWith("context is nil")) + }) + }) + + When("k8sClient is nil", func() { + JustBeforeEach(func() { + k8sClient = nil + }) + It("should panic", func() { + fn := func() { + _ = r.PreReconfigure(ctx, k8sClient, vm, moVM, configSpec) + } + Expect(fn).To(PanicWith("k8sClient is nil")) + }) + }) + + When("moVM.config is nil", func() { + BeforeEach(func() { + moVM.Config = nil + }) + It("should panic", func() { + fn := func() { + _ = r.PreReconfigure(ctx, k8sClient, vm, moVM, configSpec) + } + Expect(fn).To(PanicWith("moVM.config is nil")) + }) + }) + + When("vm is nil", func() { + BeforeEach(func() { + vm = nil + }) + It("should panic", func() { + fn := func() { + _ = r.PreReconfigure(ctx, k8sClient, vm, moVM, configSpec) + } + Expect(fn).To(PanicWith("vm is nil")) + }) + }) + + When("configSpec is nil", func() { + BeforeEach(func() { + configSpec = nil + }) + It("should panic", func() { + fn := func() { + _ = r.PreReconfigure(ctx, k8sClient, vm, moVM, configSpec) + } + Expect(fn).To(PanicWith("configSpec is nil")) + }) + }) + + When("encryptionClassName is empty", func() { + BeforeEach(func() { + vm.Spec.Crypto.ClassName = "" + }) + When("vm has EncryptionClassReady condition", func() { + BeforeEach(func() { + vm.Status.Conditions = []metav1.Condition{ + { + Type: vmopv1.VirtualMachineEncryptionClassReady, + }, + } + }) + It("should remove the condition", func() { + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady)).ToNot(BeNil()) + Expect(r.PreReconfigure(ctx, k8sClient, vm, moVM, configSpec)).To(Succeed()) + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeNil()) + }) + }) + + When("updating unencrypted vm", func() { + + var ( + err error + ) + + JustBeforeEach(func() { + err = r.PreReconfigure(ctx, k8sClient, vm, moVM, configSpec) + }) + + When("adding vtpm", func() { + BeforeEach(func() { + moVM.Config.KeyId = nil + configSpec.DeviceChange = []vimtypes.BaseVirtualDeviceConfigSpec{ + &vimtypes.VirtualDeviceConfigSpec{ + Operation: vimtypes.VirtualDeviceConfigSpecOperationAdd, + Device: &vimtypes.VirtualTPM{}, + }, + } + }) + It(shouldSetEncryptionStateSyncedWithInvalidChanges, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeNil()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidChanges.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("updating unencrypted", "not add vTPM"))) + }) + }) + }) + }) + + When("encryptionClassName is not empty", func() { + + var ( + err error + ) + + JustBeforeEach(func() { + err = r.PreReconfigure(ctx, k8sClient, vm, moVM, configSpec) + }) + + When("encryption class does not exist", func() { + BeforeEach(func() { + withObjects = []ctrlclient.Object{vm} + }) + It("should set condition EncryptionClassReady to false and not set EncryptionSynced at all", func() { + Expect(err).ToNot(HaveOccurred()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.ClassReadyReasonNotFound.String())) + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeNil()) + }) + When("updating encrypted vm", func() { + DescribeTable( + shouldSetEncryptionStateSyncedWithInvalidChanges, + disallowExtraConfigSecretKeysArgs("updating encrypted", nil)..., + ) + }) + }) + + When("encryption class does exist", func() { + JustBeforeEach(func() { + k8sClient = builder.NewFakeClient(encClass) + }) + When("encryption class is not ready", func() { + When("providerID is empty", func() { + BeforeEach(func() { + encClass.Spec.KeyProvider = "" + }) + It("should set condition EncryptionClassReady to false and not set EncryptionSynced at all", func() { + Expect(err).ToNot(HaveOccurred()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.ClassReadyReasonNoProviderID.String())) + }) + }) + When("keyID is empty", func() { + BeforeEach(func() { + encClass.Spec.KeyID = "" + }) + It("should set condition EncryptionClassReady to false and not set EncryptionSynced at all", func() { + Expect(err).ToNot(HaveOccurred()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.ClassReadyReasonNoKeyID.String())) + }) + }) + When("providerID and keyID are both empty", func() { + BeforeEach(func() { + encClass.Spec.KeyProvider = "" + encClass.Spec.KeyID = "" + }) + It("should set condition EncryptionClassReady to false and not set EncryptionSynced at all", func() { + Expect(err).ToNot(HaveOccurred()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + expectedReason := crypto.ClassReadyReasonNoProviderID ^ crypto.ClassReadyReasonNoKeyID + Expect(c.Reason).To(Equal(expectedReason.String())) + }) + }) + }) + + When("encryption class is ready", func() { + It("should set condition EncryptionClassReady to true and not set EncryptionSynced at all", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeTrue()) + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeNil()) + }) + When("decrypting", func() { + BeforeEach(func() { + vm.Spec.Crypto.ClassName = "" + }) + When("vm is powered on", func() { + BeforeEach(func() { + moVM.Summary.Runtime.PowerState = vimtypes.VirtualMachinePowerStatePoweredOn + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeNil()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("decrypting", "be powered off"))) + }) + When("vm has snapshots", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithLinearChain() + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeNil()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("decrypting", "be powered off", "not have snapshots"))) + }) + }) + }) + When("vm has snapshots", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithLinearChain() + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeNil()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("decrypting", "not have snapshots"))) + }) + }) + When("vm has vtpm", func() { + BeforeEach(func() { + moVM.Config.Hardware.Device = []vimtypes.BaseVirtualDevice{ + &vimtypes.VirtualTPM{}, + } + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeNil()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("decrypting", "not have vTPM"))) + }) + }) + }) + When("encrypting", func() { + + assertIsEncrypt := func() { + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeTrue()) + ExpectWithOffset(1, conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeNil()) + ExpectWithOffset(1, configSpec.Crypto).ToNot(BeNil()) + ExpectWithOffset(1, configSpec.Crypto).To(Equal(&vimtypes.CryptoSpecEncrypt{ + CryptoKeyId: vimtypes.CryptoKeyId{ + KeyId: encClass.Spec.KeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: encClass.Spec.KeyProvider, + }, + }, + })) + } + + BeforeEach(func() { + moVM.Config.KeyId = nil + }) + When("vm is powered on", func() { + BeforeEach(func() { + moVM.Summary.Runtime.PowerState = vimtypes.VirtualMachinePowerStatePoweredOn + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeTrue()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("encrypting", "be powered off"))) + }) + }) + When("vm has snapshots", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithLinearChain() + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeTrue()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("encrypting", "not have snapshots"))) + }) + }) + When("vm is powered off with no snapshots", func() { + It("should encrypt the vm", func() { + assertIsEncrypt() + }) + }) + }) + When("recrypting", func() { + When("new providerID", func() { + BeforeEach(func() { + encClass.Spec.KeyProvider += "2" + }) + When("vm has snapshot tree", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithTree() + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeTrue()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("recrypting", "not have snapshot tree"))) + }) + }) + }) + When("new keyID", func() { + BeforeEach(func() { + encClass.Spec.KeyID += "2" + }) + When("vm has snapshot tree", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithTree() + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeTrue()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("recrypting", "not have snapshot tree"))) + }) + }) + }) + When("new providerID and new keyID", func() { + BeforeEach(func() { + encClass.Spec.KeyProvider += "2" + encClass.Spec.KeyID += "2" + }) + When("vm has snapshot tree", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithTree() + }) + It(shouldSetEncryptionStateSyncedWithInvalidState, func() { + Expect(err).ToNot(HaveOccurred()) + Expect(conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeTrue()) + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidState.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced("recrypting", "not have snapshot tree"))) + }) + }) + + assertIsRecrypt := func() { + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, conditions.IsTrue(vm, vmopv1.VirtualMachineEncryptionClassReady)).To(BeTrue()) + ExpectWithOffset(1, conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced)).To(BeNil()) + ExpectWithOffset(1, configSpec.Crypto).ToNot(BeNil()) + ExpectWithOffset(1, configSpec.Crypto).To(Equal(&vimtypes.CryptoSpecShallowRecrypt{ + NewKeyId: vimtypes.CryptoKeyId{ + KeyId: encClass.Spec.KeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: encClass.Spec.KeyProvider, + }, + }, + })) + } + When("powered off", func() { + BeforeEach(func() { + moVM.Summary.Runtime.PowerState = vimtypes.VirtualMachinePowerStatePoweredOff + }) + It("should recrypt the vm", func() { + assertIsRecrypt() + }) + When("has linear snapshot chain", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithLinearChain() + }) + It("should recrypt the vm", func() { + assertIsRecrypt() + }) + }) + }) + When("powered on", func() { + BeforeEach(func() { + moVM.Summary.Runtime.PowerState = vimtypes.VirtualMachinePowerStatePoweredOn + }) + It("should recrypt the vm", func() { + assertIsRecrypt() + }) + When("has linear snapshot chain", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithLinearChain() + }) + It("should recrypt the vm", func() { + assertIsRecrypt() + }) + }) + }) + When("suspended", func() { + BeforeEach(func() { + moVM.Summary.Runtime.PowerState = vimtypes.VirtualMachinePowerStateSuspended + }) + It("should recrypt the vm", func() { + assertIsRecrypt() + }) + When("has linear snapshot chain", func() { + BeforeEach(func() { + moVM.Snapshot = getSnapshotInfoWithLinearChain() + }) + It("should recrypt the vm", func() { + assertIsRecrypt() + }) + }) + }) + }) + + }) + When("updating encrypted vm", func() { + DescribeTable( + shouldSetEncryptionStateSyncedWithInvalidChanges, + disallowExtraConfigSecretKeysArgs("updating encrypted", + &byokv1.EncryptionClass{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + Name: "my-encryption-class", + }, + Spec: byokv1.EncryptionClassSpec{ + KeyProvider: "my-provider-id", + KeyID: "my-key-id", + }, + })..., + ) + }) + }) + }) + }) +}) + +const ( + shouldSetEncryptionStateSyncedWithInvalidState = "should set EncryptionSynced to false w InvalidState" + shouldSetEncryptionStateSyncedWithInvalidChanges = "should set EncryptionSynced to false w InvalidChanges" +) + +func disallowExtraConfigSecretKeysArgs( + op string, + encClass *byokv1.EncryptionClass) []any { + + return []any{ + func(key string) { + vm := &vmopv1.VirtualMachine{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + Name: "my-vm", + }, + Spec: vmopv1.VirtualMachineSpec{ + Crypto: vmopv1.VirtualMachineCryptoSpec{ + ClassName: "my-encryption-class", + }, + }, + } + + withObjects := []ctrlclient.Object{vm} + if encClass != nil { + withObjects = append(withObjects, encClass) + } + + Expect(crypto.New().PreReconfigure( + context.Background(), + builder.NewFakeClient(withObjects...), + vm, + mo.VirtualMachine{ + Config: &vimtypes.VirtualMachineConfigInfo{ + KeyId: &vimtypes.CryptoKeyId{ + KeyId: "my-key-id", + ProviderId: &vimtypes.KeyProviderId{ + Id: "my-provider-id", + }, + }, + }, + }, + &vimtypes.VirtualMachineConfigSpec{ + ExtraConfig: []vimtypes.BaseOptionValue{ + &vimtypes.OptionValue{ + Key: key, + Value: "", + }, + }, + })).To(Succeed()) + + if encClass == nil || encClass.Spec.KeyProvider == "" || encClass.Spec.KeyID == "" { + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionClassReady) + Expect(c).ToNot(BeNil()) + + var ( + expectedStatus metav1.ConditionStatus + expectedReason crypto.ClassReadyReason + ) + + switch { + case encClass == nil: + expectedStatus = metav1.ConditionFalse + expectedReason = crypto.ClassReadyReasonNotFound + case encClass.Spec.KeyProvider != "" && encClass.Spec.KeyID != "": + expectedStatus = metav1.ConditionTrue + default: + expectedStatus = metav1.ConditionFalse + if encClass.Spec.KeyProvider == "" { + expectedReason.Set(crypto.ClassReadyReasonNoProviderID) + } + if encClass.Spec.KeyID == "" { + expectedReason.Set(crypto.ClassReadyReasonNoKeyID) + } + } + + Expect(c.Status).To(Equal(expectedStatus)) + if c.Status == metav1.ConditionTrue { + Expect(c.Reason).To(Equal(string(metav1.ConditionTrue))) + } else { + Expect(c.Reason).To(Equal(expectedReason.String())) + } + + return + } + + c := conditions.Get(vm, vmopv1.VirtualMachineEncryptionSynced) + Expect(c).ToNot(BeNil()) + Expect(c.Status).To(Equal(metav1.ConditionFalse)) + Expect(c.Reason).To(Equal(crypto.StateSyncedReasonInvalidChanges.String())) + Expect(c.Message).To(Equal(crypto.SprintfStateNotSynced(op, "not add/remove/modify secret key"))) + }, + func(key string) string { + return fmt.Sprintf("when key=%s", key) + }, + Entry(nil, "ancestordatafilekeys"), + Entry(nil, "cryptostate"), + Entry(nil, "datafilekey"), + Entry(nil, "encryption.required"), + Entry(nil, "encryption.required.vtpm"), + Entry(nil, "encryption.unspecified.default"), + } +} + +func getSnapshotInfoWithTree() *vimtypes.VirtualMachineSnapshotInfo { + return &vimtypes.VirtualMachineSnapshotInfo{ + CurrentSnapshot: &vimtypes.ManagedObjectReference{}, + RootSnapshotList: []vimtypes.VirtualMachineSnapshotTree{ + { + Name: "1", + ChildSnapshotList: []vimtypes.VirtualMachineSnapshotTree{ + { + Name: "1a", + }, + { + Name: "1b", + }, + }, + }, + { + Name: "2", + }, + }, + } +} + +func getSnapshotInfoWithLinearChain() *vimtypes.VirtualMachineSnapshotInfo { + return &vimtypes.VirtualMachineSnapshotInfo{ + CurrentSnapshot: &vimtypes.ManagedObjectReference{}, + RootSnapshotList: []vimtypes.VirtualMachineSnapshotTree{ + { + Name: "1", + ChildSnapshotList: []vimtypes.VirtualMachineSnapshotTree{ + { + Name: "1a", + }, + }, + }, + }, + } +} diff --git a/pkg/reconfig/crypto/crypto_reconciler_test.go b/pkg/reconfig/crypto/crypto_reconciler_test.go new file mode 100644 index 000000000..e8e17c2db --- /dev/null +++ b/pkg/reconfig/crypto/crypto_reconciler_test.go @@ -0,0 +1,23 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/vm-operator/pkg/reconfig/crypto" +) + +var _ = Describe("New", func() { + It("should return a reconciler", func() { + Expect(crypto.New()).ToNot(BeNil()) + }) +}) + +var _ = Describe("Name", func() { + It("should return crypto", func() { + Expect(crypto.New().Name()).To(Equal("crypto")) + }) +}) diff --git a/pkg/reconfig/reconfig_reconciler.go b/pkg/reconfig/reconfig_reconciler.go new file mode 100644 index 000000000..27f86228e --- /dev/null +++ b/pkg/reconfig/reconfig_reconciler.go @@ -0,0 +1,38 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package reconfig + +import ( + "context" + + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" +) + +// Reconciler is a type that participates before and after reconfiguring a VM. +type Reconciler interface { + // Name returns the unique name used to identify the reconciler. + Name() string + + // PreReconfigure attempts to update the provided ConfigSpec so a + // Reconfigure operation will reconcile the difference between the current + // and desired states managed by the reconciler. + PreReconfigure( + ctx context.Context, + k8sClient ctrlclient.Client, + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine, + configSpec *vimtypes.VirtualMachineConfigSpec) error + + // PostReconfigure responds to the result of a Reconfigure operation. + PostReconfigure( + ctx context.Context, + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine, + configSpec vimtypes.VirtualMachineConfigSpec, + reconfigErr error) error +} diff --git a/pkg/reconfig/reconfig_reconciler_context.go b/pkg/reconfig/reconfig_reconciler_context.go new file mode 100644 index 000000000..5a29c5ec7 --- /dev/null +++ b/pkg/reconfig/reconfig_reconciler_context.go @@ -0,0 +1,82 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package reconfig + +import ( + "context" + "maps" + + ctxgen "github.com/vmware-tanzu/vm-operator/pkg/context/generic" +) + +type contextKeyType uint8 + +const contextKeyValue contextKeyType = 0 + +type contextValueType map[string]Reconciler + +// Register registers the reconciler in the provided context. +// Please note, if two reconcilers have the same name, the last one registered +// takes precedence. +func Register(ctx context.Context, r Reconciler) { + ctxgen.SetContext( + ctx, + contextKeyValue, + func(curVal contextValueType) contextValueType { + curVal[r.Name()] = r + return curVal + }) +} + +// FromContext returns the list of registered reconcilers. +func FromContext(ctx context.Context) []Reconciler { + return ctxgen.FromContext( + ctx, + contextKeyValue, + func(val contextValueType) []Reconciler { + var list []Reconciler + for _, r := range val { + list = append(list, r) + } + return list + }) +} + +// WithContext returns a new context with a new reconcilers map, with the +// provided context as the parent. +func WithContext(parent context.Context) context.Context { + return ctxgen.WithContext( + parent, + contextKeyValue, + func() contextValueType { return contextValueType{} }) +} + +// NewContext returns a new context with a new reconcilers map. +func NewContext() context.Context { + return ctxgen.NewContext( + contextKeyValue, + func() contextValueType { return contextValueType{} }) +} + +// ValidateContext returns true if the provided context contains the reconcilers +// map. +func ValidateContext(ctx context.Context) bool { + return ctxgen.ValidateContext[contextValueType](ctx, contextKeyValue) +} + +// JoinContext returns a new context that contains a reference to the +// reconcilers map from the specified context. +// This function panics if the provided context does not contain a reconcilers +// map. +// This function is thread-safe. +func JoinContext(left, right context.Context) context.Context { + return ctxgen.JoinContext( + left, + right, + contextKeyValue, + func(dst, src contextValueType) contextValueType { + maps.Copy(dst, src) + return dst + }) +} diff --git a/pkg/reconfig/reconfig_reconciler_context_test.go b/pkg/reconfig/reconfig_reconciler_context_test.go new file mode 100644 index 000000000..2341d29ad --- /dev/null +++ b/pkg/reconfig/reconfig_reconciler_context_test.go @@ -0,0 +1,232 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package reconfig_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/vmware/govmomi/vim25/mo" + vimtypes "github.com/vmware/govmomi/vim25/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" + "github.com/vmware-tanzu/vm-operator/pkg/reconfig" +) + +var _ = Describe("FromContext", func() { + var ( + ctx context.Context + ) + BeforeEach(func() { + ctx = context.Background() + }) + When("ctx is nil", func() { + BeforeEach(func() { + ctx = nil + }) + It("should panic", func() { + fn := func() { + _ = reconfig.FromContext(ctx) + } + Expect(fn).To(PanicWith("context is nil")) + }) + }) + When("value is missing from context", func() { + It("should panic", func() { + fn := func() { + _ = reconfig.FromContext(ctx) + } + Expect(fn).To(PanicWith("value is missing from context")) + }) + }) + When("map is present in context", func() { + BeforeEach(func() { + ctx = reconfig.NewContext() + }) + It("should return the value", func() { + Expect(reconfig.FromContext(ctx)).To(Equal([]reconfig.Reconciler(nil))) + }) + }) +}) + +var _ = Describe("ValidateContext", func() { + var ( + ctx context.Context + ) + BeforeEach(func() { + ctx = context.Background() + }) + When("ctx is nil", func() { + BeforeEach(func() { + ctx = nil + }) + It("should panic", func() { + fn := func() { + _ = reconfig.ValidateContext(ctx) + } + Expect(fn).To(PanicWith("context is nil")) + }) + }) + When("value is missing from context", func() { + It("should return false", func() { + Expect(reconfig.ValidateContext(ctx)).To(BeFalse()) + }) + }) + When("value is present in context", func() { + BeforeEach(func() { + ctx = reconfig.NewContext() + }) + It("should return true", func() { + Expect(reconfig.ValidateContext(ctx)).To(BeTrue()) + }) + }) +}) + +type reconciler struct { + name string +} + +func (r reconciler) Name() string { + return r.name +} + +func (r reconciler) PreReconfigure( + ctx context.Context, + k8sClient ctrlclient.Client, + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine, + configSpec *vimtypes.VirtualMachineConfigSpec) error { + + return nil +} + +func (r reconciler) PostReconfigure( + ctx context.Context, + vm *vmopv1.VirtualMachine, + moVM mo.VirtualMachine, + configSpec vimtypes.VirtualMachineConfigSpec, + reconfigErr error) error { + + return nil +} + +var _ = Describe("JoinContext", func() { + var ( + left context.Context + right context.Context + ) + BeforeEach(func() { + left = context.Background() + right = context.Background() + }) + When("left context is nil", func() { + BeforeEach(func() { + left = nil + }) + It("should panic", func() { + fn := func() { + _ = reconfig.JoinContext(left, right) + } + Expect(fn).To(PanicWith("left context is nil")) + }) + }) + When("right context is nil", func() { + BeforeEach(func() { + right = nil + }) + It("should panic", func() { + fn := func() { + _ = reconfig.JoinContext(left, right) + } + Expect(fn).To(PanicWith("right context is nil")) + }) + }) + When("value is missing from context", func() { + It("should panic", func() { + fn := func() { + _ = reconfig.JoinContext(left, right) + } + Expect(fn).To(PanicWith("value is missing from context")) + }) + }) + When("the left context has the map", func() { + BeforeEach(func() { + left = reconfig.NewContext() + }) + It("should return the left context", func() { + ctx := reconfig.JoinContext(left, right) + Expect(ctx).ToNot(BeNil()) + Expect(reconfig.ValidateContext(ctx)).To(BeTrue()) + Expect(ctx).To(Equal(left)) + }) + }) + When("the right context has the map", func() { + BeforeEach(func() { + right = reconfig.NewContext() + }) + It("should return a new context", func() { + ctx := reconfig.JoinContext(left, right) + Expect(ctx).ToNot(BeNil()) + Expect(reconfig.ValidateContext(ctx)).To(BeTrue()) + }) + }) + When("both contexts have the map", func() { + var ( + v0 reconfig.Reconciler + v1 reconfig.Reconciler + v2a reconfig.Reconciler + v2b reconfig.Reconciler + ) + BeforeEach(func() { + v0 = &reconciler{name: "r0"} + v1 = &reconciler{name: "r1"} + v2a = &reconciler{name: "r2"} + v2b = &reconciler{name: "r2"} + + left = reconfig.NewContext() + right = reconfig.NewContext() + + reconfig.Register(left, v0) + reconfig.Register(right, v1) + reconfig.Register(left, v2a) + reconfig.Register(right, v2b) + + Expect(reconfig.FromContext(left)).To(ContainElements(v0, v2a)) + Expect(reconfig.FromContext(right)).To(ContainElements(v1, v2b)) + }) + It("should return the left context with key/value pairs from the right", func() { + ctx := reconfig.JoinContext(left, right) + Expect(ctx).ToNot(BeNil()) + Expect(reconfig.ValidateContext(ctx)).To(BeTrue()) + Expect(ctx).To(Equal(left)) + Expect(reconfig.FromContext(ctx)).To(ContainElements(v0, v1, v2b)) + }) + }) +}) + +var _ = Describe("NewContext", func() { + It("should return a valid context", func() { + Expect(reconfig.ValidateContext(reconfig.NewContext())).To(BeTrue()) + }) +}) + +var _ = Describe("WithContext", func() { + When("parent is nil", func() { + It("should panic", func() { + fn := func() { + //nolint:staticcheck + _ = reconfig.WithContext(nil) + } + Expect(fn).To(PanicWith("parent context is nil")) + }) + }) + When("parent is not nil", func() { + It("should return a context", func() { + ctx := reconfig.WithContext(context.Background()) + Expect(ctx).ToNot(BeNil()) + }) + }) +}) diff --git a/pkg/reconfig/reconfig_reconciler_suite_test.go b/pkg/reconfig/reconfig_reconciler_suite_test.go new file mode 100644 index 000000000..454975d3c --- /dev/null +++ b/pkg/reconfig/reconfig_reconciler_suite_test.go @@ -0,0 +1,24 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package reconfig_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/klog/v2" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +func init() { + klog.SetOutput(GinkgoWriter) + logf.SetLogger(klog.Background()) +} + +func TestSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Reconfig Reconciler Test Suite") +} diff --git a/pkg/util/vsphere/vm/crypto/crypto_spec.go b/pkg/util/vsphere/vm/crypto/crypto_spec.go new file mode 100644 index 000000000..37ba2ab64 --- /dev/null +++ b/pkg/util/vsphere/vm/crypto/crypto_spec.go @@ -0,0 +1,104 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto + +import ( + "errors" + + vimtypes "github.com/vmware/govmomi/vim25/types" +) + +// ErrEmptyKeyID is returned from UpdateConfigSpec when a non-empty providerID +// is sent with an empty keyID. +var ErrEmptyKeyID = errors.New("newKeyID is empty with non-empty newProviderID") + +// GetCryptoSpec returns the CryptoSpec needed to update the VM based on the +// current state of the VM and the provided providerID and keyID information. +// +// - Encrypt the VM if it is not currently encrypted and the providerID and +// keyID parameters are non-empty. +// +// - Decrypt the VM if it is currently encrypted and the providerID and keyID +// parameters are empty. +// +// - Recrypt (shallow) the VM if it is currently encrypted and the provider ID +// and keyID parameters are non-empty and either one is different from when +// the VM was last encrypted/recrypted. +// +// The VM is determined to be encrypted if configInfo.keyID != nil && +// configInfo.keyId.keyId != "". +// +// A nil value is returned if no changes are needed. +func GetCryptoSpec( + configInfo vimtypes.VirtualMachineConfigInfo, + newProviderID, newKeyID string) (vimtypes.BaseCryptoSpec, error) { + + if newProviderID != "" && newKeyID == "" { + return nil, ErrEmptyKeyID + } + + var ( + curKeyID string + curProID string + ) + + // Check to see if the VM is currently encrypted and record the current + // provider ID and key ID. + if kid := configInfo.KeyId; kid != nil { + curKeyID = kid.KeyId + if pid := kid.ProviderId; pid != nil { + curProID = pid.Id + } + } + + switch { + case curKeyID != "" && newKeyID == "": + // + // Decrypt + // + + return &vimtypes.CryptoSpecDecrypt{}, nil + + case curKeyID == "" && newKeyID != "": + // + // Encrypt + // + + var pid *vimtypes.KeyProviderId + if newProviderID != "" { + pid = &vimtypes.KeyProviderId{ + Id: newProviderID, + } + } + + return &vimtypes.CryptoSpecEncrypt{ + CryptoKeyId: vimtypes.CryptoKeyId{ + ProviderId: pid, + KeyId: newKeyID, + }, + }, nil + + case curProID+curKeyID != newProviderID+newKeyID: + // + // Recrypt + // + + var pid *vimtypes.KeyProviderId + if newProviderID != "" { + pid = &vimtypes.KeyProviderId{ + Id: newProviderID, + } + } + + return &vimtypes.CryptoSpecShallowRecrypt{ + NewKeyId: vimtypes.CryptoKeyId{ + ProviderId: pid, + KeyId: newKeyID, + }, + }, nil + + } + + return nil, nil +} diff --git a/pkg/util/vsphere/vm/crypto/crypto_spec_test.go b/pkg/util/vsphere/vm/crypto/crypto_spec_test.go new file mode 100644 index 000000000..b3fa03a94 --- /dev/null +++ b/pkg/util/vsphere/vm/crypto/crypto_spec_test.go @@ -0,0 +1,247 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto_test + +import ( + "bytes" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + vimtypes "github.com/vmware/govmomi/vim25/types" + + "github.com/vmware-tanzu/vm-operator/pkg/util/vsphere/vm/crypto" +) + +const ( + fakeProviderID = "fakeProviderID" + fakeKeyID = "fakeKeyID" + fakeProviderID2 = fakeProviderID + "2" + fakeKeyID2 = fakeKeyID + "2" +) + +var _ = DescribeTable("GetCryptoSpec", + func( + should string, + configInfo vimtypes.VirtualMachineConfigInfo, + newProviderID, newKeyID string, + expectedCryptoSpec vimtypes.BaseCryptoSpec, + expectedErr error) { + + actualCryptoSpec, actualErr := crypto.GetCryptoSpec( + configInfo, + newProviderID, newKeyID) + + if expectedErr != nil { + Expect(actualErr).To(HaveOccurred()) + Expect(actualErr).To(MatchError(expectedErr)) + } else { + Expect(actualErr).ToNot(HaveOccurred()) + } + + if expectedCryptoSpec == nil { + Expect(actualCryptoSpec).To(BeNil()) + } else { + Expect(actualCryptoSpec).To(Equal(expectedCryptoSpec)) + } + }, + func( + should string, + configInfo vimtypes.VirtualMachineConfigInfo, + newProviderID, newKeyID string, + expectedCryptoSpec vimtypes.BaseCryptoSpec, + expectedErr error) string { + + var ( + w bytes.Buffer + curProviderID string + curKeyID string + ) + + if kid := configInfo.KeyId; kid != nil { + curKeyID = kid.KeyId + if pid := kid.ProviderId; pid != nil { + curProviderID = pid.Id + } + } + + fmt.Fprintf(&w, "%s w vm is ", should) + + if curKeyID == "" { + w.WriteString("not ") + } + + w.WriteString("encrypted") + + buildPropString := func(name, curVal, newVal string) { + fmt.Fprintf(&w, ", %s is ", name) + + if newVal != "" { + w.WriteString("not ") + } + + w.WriteString("empty") + + if curVal != "" { + w.WriteString(" and ") + if curVal != newVal { + w.WriteString("not ") + } + w.WriteString("same as current value") + } + } + + buildPropString("providerID", curProviderID, newProviderID) + buildPropString("keyID", curKeyID, newKeyID) + + return w.String() + }, + + Entry( + nil, + "error", + vimtypes.VirtualMachineConfigInfo{}, + fakeProviderID, "", + nil, + crypto.ErrEmptyKeyID, + ), + Entry( + nil, + "error", + vimtypes.VirtualMachineConfigInfo{ + KeyId: &vimtypes.CryptoKeyId{ + KeyId: fakeKeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: fakeProviderID, + }, + }, + }, + fakeProviderID, "", + nil, + crypto.ErrEmptyKeyID, + ), + + Entry( + nil, + "noop", + vimtypes.VirtualMachineConfigInfo{}, + "", "", + nil, + nil, + ), + Entry( + nil, + "noop", + vimtypes.VirtualMachineConfigInfo{ + KeyId: &vimtypes.CryptoKeyId{ + KeyId: fakeKeyID, + }, + }, + "", fakeKeyID, + nil, + nil, + ), + Entry( + nil, + "noop", + vimtypes.VirtualMachineConfigInfo{ + KeyId: &vimtypes.CryptoKeyId{ + KeyId: fakeKeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: fakeProviderID, + }, + }, + }, + fakeProviderID, fakeKeyID, + nil, + nil, + ), + + Entry( + nil, + "encrypt", + vimtypes.VirtualMachineConfigInfo{}, + "", fakeKeyID, + &vimtypes.CryptoSpecEncrypt{ + CryptoKeyId: vimtypes.CryptoKeyId{ + KeyId: fakeKeyID, + }, + }, + nil, + ), + Entry( + nil, + "encrypt", + vimtypes.VirtualMachineConfigInfo{}, + fakeProviderID, fakeKeyID, + &vimtypes.CryptoSpecEncrypt{ + CryptoKeyId: vimtypes.CryptoKeyId{ + KeyId: fakeKeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: fakeProviderID, + }, + }, + }, + nil, + ), + + Entry( + nil, + "recrypt", + vimtypes.VirtualMachineConfigInfo{ + KeyId: &vimtypes.CryptoKeyId{ + KeyId: fakeKeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: fakeProviderID, + }, + }, + }, + "", fakeKeyID2, + &vimtypes.CryptoSpecShallowRecrypt{ + NewKeyId: vimtypes.CryptoKeyId{ + KeyId: fakeKeyID2, + }, + }, + nil, + ), + Entry( + nil, + "recrypt", + vimtypes.VirtualMachineConfigInfo{ + KeyId: &vimtypes.CryptoKeyId{ + KeyId: fakeKeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: fakeProviderID, + }, + }, + }, + fakeProviderID2, fakeKeyID2, + &vimtypes.CryptoSpecShallowRecrypt{ + NewKeyId: vimtypes.CryptoKeyId{ + KeyId: fakeKeyID2, + ProviderId: &vimtypes.KeyProviderId{ + Id: fakeProviderID2, + }, + }, + }, + nil, + ), + + Entry( + nil, + "decrypt", + vimtypes.VirtualMachineConfigInfo{ + KeyId: &vimtypes.CryptoKeyId{ + KeyId: fakeKeyID, + ProviderId: &vimtypes.KeyProviderId{ + Id: fakeProviderID, + }, + }, + }, + "", "", + &vimtypes.CryptoSpecDecrypt{}, + nil, + ), +) diff --git a/pkg/util/vsphere/vm/crypto/crypto_suite_test.go b/pkg/util/vsphere/vm/crypto/crypto_suite_test.go new file mode 100644 index 000000000..41b0e15cf --- /dev/null +++ b/pkg/util/vsphere/vm/crypto/crypto_suite_test.go @@ -0,0 +1,24 @@ +// Copyright (c) 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crypto_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +var suite = builder.NewTestSuite() + +func TestCrypto(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "vSphere VirtualMachine Crypto Suite") +} + +var _ = BeforeSuite(suite.BeforeSuite) + +var _ = AfterSuite(suite.AfterSuite)