diff --git a/frontend/packages/kubevirt-plugin/src/components/edit-button.tsx b/frontend/packages/kubevirt-plugin/src/components/edit-button.tsx new file mode 100644 index 00000000000..6c2d5b19ea4 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/edit-button.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +export const EditButton: React.FC = (props) => { + const { canEdit, onClick, children } = props; + + if (canEdit) { + return ( + + ); + } + + return null; +}; + +type EditButtonProps = { + children: any; + canEdit: boolean; + onClick: React.MouseEventHandler; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/flavor-text.tsx b/frontend/packages/kubevirt-plugin/src/components/flavor-text.tsx new file mode 100644 index 00000000000..a27149fedb1 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/flavor-text.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { convertToBaseValue, humanizeDecimalBytes } from '@console/internal/components/utils'; +import { getFlavor, vCPUCount, getCPU, getMemory, asVM } from '../selectors/vm'; +import { VMLikeEntityKind } from '../types'; + +export const FlavorText: React.FC = (props) => { + const { vmLike } = props; + const vm = asVM(vmLike); + + const flavor = _.capitalize(getFlavor(vmLike)); + + const vcpusCount = vCPUCount(getCPU(vm)); + const vcpusText = `${vcpusCount} vCPU${vcpusCount > 1 ? 's' : ''}`; + + const memoryBase = convertToBaseValue(getMemory(vm)); + const memoryText = humanizeDecimalBytes(memoryBase).string; + + return <>{`${flavor || ''}${flavor ? ': ' : ''}${vcpusText}, ${memoryText} Memory`}; +}; + +type FlavorTextProps = { + vmLike: VMLikeEntityKind; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/delete-device-modal/delete-device-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/delete-device-modal/delete-device-modal.tsx index 46f720d74fc..6db91868461 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/delete-device-modal/delete-device-modal.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/delete-device-modal/delete-device-modal.tsx @@ -9,7 +9,7 @@ import { import { k8sPatch } from '@console/internal/module/k8s'; import { getName } from '@console/shared'; import { VMLikeEntityKind } from '../../../types'; -import { getVMLikeModel } from '../../../selectors/selectors'; +import { getVMLikeModel } from '../../../selectors/vm'; import { getRemoveDiskPatches } from '../../../k8s/patches/vm/vm-disk-patches'; import { getRemoveNicPatches } from '../../../k8s/patches/vm/vm-nic-patches'; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/index.ts b/frontend/packages/kubevirt-plugin/src/components/modals/index.ts index 6601c3928c8..31334748a83 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/index.ts +++ b/frontend/packages/kubevirt-plugin/src/components/modals/index.ts @@ -1,3 +1,5 @@ export * from './create-vm-wizard'; export * from './delete-device-modal'; export * from './modal-resource-launcher'; +export * from './vm-description-modal'; +export * from './vm-flavor-modal'; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/vm-description-modal/vm-description-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/vm-description-modal/vm-description-modal.tsx index 55476af3ac1..6f1f4befdc8 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/vm-description-modal/vm-description-modal.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/vm-description-modal/vm-description-modal.tsx @@ -9,10 +9,12 @@ import { ModalComponentProps, } from '@console/internal/components/factory'; import { k8sPatch } from '@console/internal/module/k8s'; +import { getDescription } from '../../../selectors/selectors'; import { VMLikeEntityKind } from '../../../types'; -import { getDescription, getVMLikeModel } from '../../../selectors/selectors'; +import { getVMLikeModel } from '../../../selectors/vm'; import { getUpdateDescriptionPatches } from '../../../k8s/patches/vm/vm-patches'; +// TODO: should be moved under kubevirt-plugin/src/style.scss import './_vm-description-modal.scss'; export const VMDescriptionModal = withHandlePromise((props: VMDescriptionModalProps) => { diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/_vm-flavor-modal.scss b/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/_vm-flavor-modal.scss new file mode 100644 index 00000000000..f38c609ab00 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/_vm-flavor-modal.scss @@ -0,0 +1,24 @@ +.kubevirt-vm-flavor-modal__content-custom { + height: 20em; +} + +.kubevirt-vm-flavor-modal__content-generic { + min-height: 13em; + .modal-body { + overflow-y: unset; /* Hotfix for PF3 */ + } +} + +.kubevirt-vm-flavor-modal__form { + resize: vertical; +} + +.kubevirt-vm-flavor-modal__dropdown { + .dropdown { + width: 100%; + } +} + +.kubevirt-vm-flavor-modal__dropdown-button { + width: 100%; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/index.ts b/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/index.ts new file mode 100644 index 00000000000..36b5defc1ec --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/index.ts @@ -0,0 +1 @@ +export * from './vm-flavor-modal'; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/vm-flavor-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/vm-flavor-modal.tsx new file mode 100644 index 00000000000..91853389a26 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/vm-flavor-modal/vm-flavor-modal.tsx @@ -0,0 +1,179 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import * as classNames from 'classnames'; +import { getResource } from 'kubevirt-web-ui-components'; +import { + HandlePromiseProps, + withHandlePromise, + Dropdown, + convertToBaseValue, + Firehose, +} from '@console/internal/components/utils'; +import { TemplateModel } from '@console/internal/models'; +import { + createModalLauncher, + ModalTitle, + ModalBody, + ModalComponentProps, + ModalFooter, +} from '@console/internal/components/factory'; +import { k8sPatch, TemplateKind } from '@console/internal/module/k8s'; +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; +import { VMKind, VMLikeEntityKind } from '../../../types'; +import { getFlavor, getMemory, getCPU, isVM, vCPUCount } from '../../../selectors/vm'; +import { selectVM, getFlavors } from '../../../selectors/vm-template/selectors'; +import { getUpdateFlavorPatches } from '../../../k8s/patches/vm/vm-patches'; +import { VirtualMachineModel } from '../../../models'; +import { + CUSTOM_FLAVOR, + NAMESPACE_OPENSHIFT, + TEMPLATE_TYPE_LABEL, + TEMPLATE_TYPE_BASE, +} from '../../../constants'; +import './_vm-flavor-modal.scss'; + +const MB = 1000 ** 2; + +const getId = (field: string) => `vm-flavor-modal-${field}`; +const dehumanizeMemory = (memory?: string) => { + if (!memory) { + return null; + } + + return convertToBaseValue(memory) / MB; +}; + +const VMFlavorModal = withHandlePromise((props: VMFlavornModalProps) => { + const { + vmLike, + templates, + inProgress, + errorMessage, + handlePromise, + close, + cancel, + loadError, + loaded, + } = props; + + const flattenTemplates = _.get(templates, 'data', []) as TemplateKind[]; + + const vmFlavor = getFlavor(vmLike); + const flavors = getFlavors(vmLike, flattenTemplates); + + const sourceMemory = isVM(vmLike) + ? getMemory(vmLike as VMKind) + : getMemory(selectVM(vmLike as TemplateKind)); + + const sourceCPURaw = isVM(vmLike) + ? getCPU(vmLike as VMKind) + : getCPU(selectVM(vmLike as TemplateKind)); + const sourceCPU = vCPUCount(sourceCPURaw); + + const [flavor, setFlavor] = React.useState(vmFlavor); + const [mem, setMem] = React.useState( + vmFlavor === CUSTOM_FLAVOR ? dehumanizeMemory(sourceMemory) : 1, + ); + const [cpu, setCpu] = React.useState(vmFlavor === CUSTOM_FLAVOR ? sourceCPU : 1); + + const submit = (e) => { + e.preventDefault(); + + const patches = getUpdateFlavorPatches(vmLike, flattenTemplates, flavor, cpu, `${mem}M`); + if (patches.length === 0) { + close(); + } else { + const model = isVM(vmLike) ? VirtualMachineModel : TemplateModel; + const promise = k8sPatch(model, vmLike, patches); + handlePromise(promise).then(close); // eslint-disable-line promise/catch-or-return + } + }; + + const topClass = classNames('modal-content', { + 'kubevirt-vm-flavor-modal__content-custom': flavor === CUSTOM_FLAVOR, + 'kubevirt-vm-flavor-modal__content-generic': flavor !== CUSTOM_FLAVOR, + }); + + return ( +
+ Edit Flavor + +
+ + setFlavor(f)} + selectedKey={_.capitalize(flavor) || CUSTOM_FLAVOR} + title={_.capitalize(flavor)} + className="kubevirt-vm-flavor-modal__dropdown" + buttonClassName="kubevirt-vm-flavor-modal__dropdown-button" + /> + + + {flavor === CUSTOM_FLAVOR && ( + + + setCpu(parseInt(v, 10) || 1)} + aria-label="CPU count" + /> + + + setMem(parseInt(v, 10) || 1)} + aria-label="Memory" + /> + + + )} +
+
+ + + + +
+ ); +}); + +const VMFlavorModalFirehose = (props) => { + const resources = [ + getResource(TemplateModel, { + namespace: NAMESPACE_OPENSHIFT, + prop: 'templates', + matchLabels: { [TEMPLATE_TYPE_LABEL]: TEMPLATE_TYPE_BASE }, + }), + ]; + + return ( + + + + ); +}; + +export type VMFlavornModalProps = HandlePromiseProps & + ModalComponentProps & { + vmLike: VMLikeEntityKind; + templates?: any; + loadError?: any; + loaded: boolean; + }; + +export const vmFlavorModal = createModalLauncher(VMFlavorModalFirehose); diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/create-disk-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/create-disk-row.tsx index b10c27cc994..57d1d23e21f 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/create-disk-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/create-disk-row.tsx @@ -13,8 +13,7 @@ import { StorageClassModel } from '@console/internal/models'; import { getName } from '@console/shared'; import { useSafetyFirst } from '@console/internal/components/safety-first'; import { k8sPatch, K8sResourceKind } from '@console/internal/module/k8s'; -import { getVmPreferableDiskBus } from '../../selectors/vm'; -import { getVMLikeModel } from '../../selectors/selectors'; +import { getVmPreferableDiskBus, getVMLikeModel } from '../../selectors/vm'; import { getAddDiskPatches } from '../../k8s/patches/vm/vm-disk-patches'; import { VMLikeEntityKind } from '../../types'; import { ValidationErrorType } from '../../utils/validations/common'; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx index e5d683b2e26..8ba6d208dfb 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx @@ -11,9 +11,8 @@ import { TemplateModel } from '@console/internal/models'; import { BUS_VIRTIO } from '../../constants/vm'; import { deleteDeviceModal, DeviceType } from '../modals/delete-device-modal'; import { VMLikeEntityKind } from '../../types'; -import { getDiskBus } from '../../selectors/vm'; +import { getDiskBus, isVM } from '../../selectors/vm'; import { VirtualMachineModel } from '../../models'; -import { isVM } from '../../selectors/selectors'; import { VMDiskRowProps } from './types'; const menuActionDelete = (vmLikeEntity: VMLikeEntityKind, disk): KebabOption => ({ diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx index 96d1daf6363..50cb5f85594 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx @@ -12,8 +12,8 @@ import { K8sResourceKind } from '@console/internal/module/k8s'; import { sortable } from '@patternfly/react-table'; import { DataVolumeModel } from '../../models'; import { VMLikeEntityKind } from '../../types'; -import { asVM } from '../../selectors/selectors'; import { + asVM, getDataVolumeTemplates, getDisks, getVolumeDataVolumeName, diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/create-nic-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/create-nic-row.tsx index 19be2341082..b8c8615ac0c 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/create-nic-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/create-nic-row.tsx @@ -8,11 +8,10 @@ import { FormGroup, HelpBlock } from 'patternfly-react'; import { useSafetyFirst } from '@console/internal/components/safety-first'; import { k8sPatch, K8sResourceKind } from '@console/internal/module/k8s'; import { getNamespace } from '@console/shared'; -import { getVMLikeModel } from '../../selectors/selectors'; import { NetworkAttachmentDefinitionModel } from '../../models'; import { getAddNicPatches } from '../../k8s/patches/vm/vm-nic-patches'; import { VMKind, VMLikeEntityKind } from '../../types'; -import { getNetworkChoices } from '../../selectors/vm'; +import { getNetworkChoices, getVMLikeModel } from '../../selectors/vm'; import { dimensifyRow } from '../../utils/table'; import { NetworkType } from '../../constants/vm'; import { getValidationErrorMessage, getValidationErrorType } from '../../utils/validations/common'; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx index 618a262b190..875d5c7385c 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx @@ -7,7 +7,7 @@ import { BUS_VIRTIO } from '../../constants/vm'; import { deleteDeviceModal, DeviceType } from '../modals/delete-device-modal'; import { VMLikeEntityKind } from '../../types'; import { VirtualMachineModel } from '../../models'; -import { isVM } from '../../selectors/selectors'; +import { isVM } from '../../selectors/vm'; import { dimensifyRow } from '../../utils/table'; import { nicTableColumnClasses } from './utils'; import { VMNicRowProps } from './types'; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx index 1b34c897f09..0c65f8ebed8 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx @@ -7,8 +7,7 @@ import { useSafetyFirst } from '@console/internal/components/safety-first'; import { sortable } from '@patternfly/react-table'; import { createBasicLookup } from '@console/shared'; import { VMLikeEntityKind } from '../../types'; -import { asVM } from '../../selectors/selectors'; -import { getInterfaces, getNetworks, getVmPreferableNicBus } from '../../selectors/vm'; +import { getInterfaces, getNetworks, getVmPreferableNicBus, asVM } from '../../selectors/vm'; import { dimensifyHeader } from '../../utils/table'; import { VMLikeEntityTabProps } from '../vms/types'; import { NicRow } from './nic-row'; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-templates/vm-template-details.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-templates/vm-template-details.tsx index a6839183fee..011870ac271 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-templates/vm-template-details.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-templates/vm-template-details.tsx @@ -24,14 +24,16 @@ const VMTemplateDetailsFirehose: React.FC = (pro getResource(DataVolumeModel, { namespace, optional: true, prop: 'datavolumes' }), ]; + const otherProps = { template }; + return (
{hasDataVolumes ? ( - + ) : ( - + )}
); @@ -65,7 +67,7 @@ const VMTemplateDetails: React.FC = (props) => {
- +
diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-templates/vm-template-resource.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-templates/vm-template-resource.tsx index f223516a27d..057f435c818 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-templates/vm-template-resource.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-templates/vm-template-resource.tsx @@ -4,7 +4,6 @@ import { getOperatingSystem, getWorkloadProfile, getVmTemplate, - getFlavor, BootOrder, getBootableDevicesInOrder, TemplateSource, @@ -15,6 +14,9 @@ import { TemplateKind, K8sResourceKind } from '@console/internal/module/k8s'; import { getBasicID, prefixedID } from '../../utils'; import { vmDescriptionModal } from '../modals/vm-description-modal'; import { getDescription } from '../../selectors/selectors'; +import { vmFlavorModal } from '../modals'; +import { FlavorText } from '../flavor-text'; +import { EditButton } from '../edit-button'; import { VMTemplateLink } from './vm-template-link'; import './_vm-template-resource.scss'; @@ -30,18 +32,14 @@ export const VMTemplateResourceSummary: React.FC return ( -
- Description - {canUpdateTemplate && ( -
+
Description
- {description} + vmDescriptionModal({ vmLikeEntity: template })} + > + {description} +
Operating System
@@ -60,6 +58,7 @@ export const VMTemplateResourceSummary: React.FC export const VMTemplateDetailsList: React.FC = ({ template, dataVolumes, + canUpdateTemplate, }) => { const id = getBasicID(template); const sortedBootableDevices = getBootableDevicesInOrder(template); @@ -75,7 +74,11 @@ export const VMTemplateDetailsList: React.FC = ({ )}
Flavor
-
{getFlavor(template) || DASH}
+
+ vmFlavorModal({ vmLike: template })}> + + +
Provision Source
{dataVolumes ? ( @@ -91,6 +94,7 @@ export const VMTemplateDetailsList: React.FC = ({ type VMTemplateResourceListProps = { template: TemplateKind; dataVolumes: K8sResourceKind[]; + canUpdateTemplate: boolean; }; type VMTemplateResourceSummaryProps = { diff --git a/frontend/packages/kubevirt-plugin/src/components/vms/types.ts b/frontend/packages/kubevirt-plugin/src/components/vms/types.ts index 6f557527563..b1210538456 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vms/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/vms/types.ts @@ -1,4 +1,4 @@ -import { K8sResourceKind, PodKind } from '@console/internal/module/k8s'; +import { K8sResourceKind, PodKind, TemplateKind } from '@console/internal/module/k8s'; import { VMIKind, VMKind } from '../../types/vm'; import { VMLikeEntityKind } from '../../types'; @@ -7,6 +7,7 @@ export type VMTabProps = { vmi?: VMIKind; pods?: PodKind[]; migrations?: K8sResourceKind[]; + templates?: TemplateKind[]; }; export type VMLikeEntityTabProps = { diff --git a/frontend/packages/kubevirt-plugin/src/components/vms/vm-details.tsx b/frontend/packages/kubevirt-plugin/src/components/vms/vm-details.tsx index fd77747d783..7212037bac4 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vms/vm-details.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vms/vm-details.tsx @@ -10,7 +10,7 @@ import { asAccessReview, } from '@console/internal/components/utils'; import { getNamespace } from '@console/shared'; -import { K8sResourceKind, PodKind } from '@console/internal/module/k8s'; +import { K8sResourceKind, PodKind, TemplateKind } from '@console/internal/module/k8s'; import { ServiceModel } from '@console/internal/models'; import { ServicesList } from '@console/internal/components/service'; import { VMKind, VMIKind } from '../../types'; @@ -19,25 +19,32 @@ import { VirtualMachineInstanceModel } from '../../models'; import { VMResourceSummary, VMDetailsList } from './vm-resource'; import { VMTabProps } from './types'; -export const VMDetailsFirehose: React.FC = ({ obj: vm, vmi, pods, migrations }) => { +export const VMDetailsFirehose: React.FC = ({ + obj: vm, + vmi, + pods, + migrations, + templates, +}) => { const resources = [getResource(ServiceModel, { namespace: getNamespace(vm), prop: 'services' })]; return (
- +
); }; const VMDetails: React.FC = (props) => { - const { vm, vmi, pods, migrations, ...restProps } = props; + const { vm, vmi, pods, migrations, templates, ...restProps } = props; const mainResources = { vm, vmi, pods, migrations, + templates, }; const vmServicesData = getServicesForVm(getLoadedData(props.services, []), vm); @@ -54,7 +61,7 @@ const VMDetails: React.FC = (props) => {
- +
@@ -72,4 +79,5 @@ type VMDetailsProps = { migrations?: K8sResourceKind[]; vmi?: VMIKind; services?: FirehoseResult; + templates?: TemplateKind[]; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/vms/vm-resource.tsx b/frontend/packages/kubevirt-plugin/src/components/vms/vm-resource.tsx index 0e8596adca6..cd39b4ce419 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vms/vm-resource.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vms/vm-resource.tsx @@ -6,7 +6,6 @@ import { getWorkloadProfile, getVmTemplate, getNodeName, - getFlavor, VmStatuses, BootOrder, isVmOff, @@ -19,9 +18,11 @@ import { PodModel } from '@console/internal/models'; import { VMKind, VMIKind } from '../../types'; import { VMTemplateLink } from '../vm-templates/vm-template-link'; import { getBasicID, prefixedID } from '../../utils'; -import { vmDescriptionModal } from '../modals/vm-description-modal'; +import { vmDescriptionModal, vmFlavorModal } from '../modals'; import { getDescription } from '../../selectors/selectors'; import { getVMStatus } from '../../statuses/vm/vm'; +import { FlavorText } from '../flavor-text'; +import { EditButton } from '../edit-button'; import './_vm-resource.scss'; @@ -33,18 +34,11 @@ export const VMResourceSummary: React.FC = ({ vm, canUpd return ( -
- Description - {canUpdateVM && ( -
+
Description
- {description} + vmDescriptionModal({ vmLikeEntity: vm })}> + {description} +
Operating System
@@ -58,7 +52,13 @@ export const VMResourceSummary: React.FC = ({ vm, canUpd ); }; -export const VMDetailsList: React.FC = ({ vm, vmi, pods, migrations }) => { +export const VMDetailsList: React.FC = ({ + vm, + vmi, + pods, + migrations, + canUpdateVM, +}) => { const id = getBasicID(vm); const vmStatus = getVMStatus(vm, pods, migrations); const { launcherPod } = vmStatus; @@ -100,7 +100,11 @@ export const VMDetailsList: React.FC = ({ vm, vmi, pods, mi
Node
{}
Flavor
-
{getFlavor(vm) || DASH}
+
+ vmFlavorModal({ vmLike: vm })}> + + +
Workload Profile
{getWorkloadProfile(vm) || DASH}
@@ -117,4 +121,5 @@ type VMResourceListProps = { pods?: PodKind[]; migrations?: any[]; vmi?: VMIKind; + canUpdateVM: boolean; }; diff --git a/frontend/packages/kubevirt-plugin/src/constants/index.ts b/frontend/packages/kubevirt-plugin/src/constants/index.ts index 034fabedd65..772837351d0 100644 --- a/frontend/packages/kubevirt-plugin/src/constants/index.ts +++ b/frontend/packages/kubevirt-plugin/src/constants/index.ts @@ -1,3 +1,4 @@ export * from './vm'; export * from './vm-templates'; export * from './cdi'; +export * from './namespace'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/namespace.ts b/frontend/packages/kubevirt-plugin/src/constants/namespace.ts new file mode 100644 index 00000000000..37e37fa28d8 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/namespace.ts @@ -0,0 +1 @@ +export const NAMESPACE_OPENSHIFT = 'openshift'; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm-template/index.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm-template/index.ts index 6b5ee6496d8..46d21c55694 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm-template/index.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm-template/index.ts @@ -1,6 +1,6 @@ import { TemplateKind, Patch } from '@console/internal/module/k8s'; import { VMLikeEntityKind, VMKind } from '../../../types'; -import { isVM } from '../../../selectors/selectors'; +import { isVM } from '../../../selectors/vm'; import { selectVM } from '../../../selectors/vm-template/selectors'; export const addPrefixToPatch = (prefix: string, patch: Patch): Patch => ({ diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-patches.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-patches.ts index 6356cdffaa8..7e7c9b60698 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-patches.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-patches.ts @@ -1,6 +1,158 @@ -import { Patch } from '@console/internal/module/k8s'; -import { VMLikeEntityKind } from '../../../types'; +import * as _ from 'lodash'; +import { Patch, TemplateKind } from '@console/internal/module/k8s'; +import { VMLikeEntityKind, VMKind, CPU } from '../../../types'; import { getAnnotations, getDescription } from '../../../selectors/selectors'; +import { getFlavor, getCPU, getMemory, isVM, parseCPU, DEFAULT_CPU } from '../../../selectors/vm'; +import { CUSTOM_FLAVOR, TEMPLATE_FLAVOR_LABEL } from '../../../constants'; +import { selectVM, getTemplateForFlavor } from '../../../selectors/vm-template/selectors'; +import { getVMLikePatches } from '../vm-template'; +import { isCPUEqual } from '../../../utils'; + +const getLabelsPatch = (vmLike: VMLikeEntityKind): Patch => { + if (!_.has(vmLike.metadata, 'labels')) { + return { + op: 'add', + path: '/metadata/labels', + value: {}, + }; + } + return null; +}; + +const getDomainPatch = (vm: VMKind): Patch => { + let patch: Patch = null; + if (!_.has(vm, 'spec')) { + patch = { + op: 'add', + path: '/spec', + value: { + template: { + spec: { + domain: {}, + }, + }, + }, + }; + } else if (!_.has(vm.spec, 'template')) { + patch = { + op: 'add', + path: '/spec/template', + value: { + spec: { + domain: {}, + }, + }, + }; + } else if (!_.has(vm.spec.template, 'spec')) { + patch = { + op: 'add', + path: '/spec/template/spec', + value: { + domain: {}, + }, + }; + } else if (!_.has(vm.spec.template.spec, 'domain')) { + patch = { + op: 'add', + path: '/spec/template/spec/domain', + value: {}, + }; + } + return patch; +}; + +const getUpdateFlavorPatch = (vmLike: VMLikeEntityKind, flavor: string): Patch[] => { + const patch = []; + if (flavor !== getFlavor(vmLike)) { + const labelKey = `${TEMPLATE_FLAVOR_LABEL}/${flavor}`.replace('~', '~0').replace('/', '~1'); + const labelPatch = getLabelsPatch(vmLike); + if (labelPatch) { + patch.push(labelPatch); + } + const flavorLabel = Object.keys(vmLike.metadata.labels || {}).find((key) => + key.startsWith(TEMPLATE_FLAVOR_LABEL), + ); + if (flavorLabel) { + const flavorParts = flavorLabel.split('/'); + if (flavorParts[flavorParts.length - 1] !== flavor) { + const escapedLabel = flavorLabel.replace('~', '~0').replace('/', '~1'); + patch.push({ + op: 'remove', + path: `/metadata/labels/${escapedLabel}`, + }); + } + } + patch.push({ + op: 'add', + path: `/metadata/labels/${labelKey}`, + value: 'true', + }); + } + return patch; +}; + +const getCpuPatch = (vm: VMKind, cpu: CPU): Patch => { + return { + op: _.has(vm.spec, 'template.spec.domain.cpu') ? 'replace' : 'add', + path: '/spec/template/spec/domain/cpu', + value: { + sockets: cpu.sockets, + cores: cpu.cores, + threads: cpu.threads, + }, + }; +}; + +const getMemoryPatch = (vm: VMKind, memory: string): Patch => { + if (!_.has(vm.spec, 'template.spec.domain.resources')) { + return { + op: 'add', + path: '/spec/template/spec/domain/resources', + value: { + requests: { + memory, + }, + }, + }; + } + if (!_.has(vm.spec, 'template.spec.domain.resources.requests')) { + return { + op: 'add', + path: '/spec/template/spec/domain/resources/requests', + value: { + memory, + }, + }; + } + return { + op: _.has(vm.spec, 'template.spec.domain.resources.requests.memory') ? 'replace' : 'add', + path: '/spec/template/spec/domain/resources/requests/memory', + value: memory, + }; +}; + +const getUpdateCpuMemoryPatch = (vm: VMKind, cpu: CPU, memory: string): Patch[] => { + const patch = []; + const vmCpu = parseCPU(getCPU(vm)); + const vmMemory = getMemory(vm); + + if (memory !== vmMemory || !isCPUEqual(cpu, vmCpu)) { + const domainPatch = getDomainPatch(vm); + if (domainPatch) { + patch.push(domainPatch); + } + } + + if (!isCPUEqual(cpu, vmCpu)) { + patch.push(getCpuPatch(vm, cpu)); + } + + if (memory !== vmMemory) { + patch.push(getMemoryPatch(vm, memory)); + } + + return patch; +}; export const getUpdateDescriptionPatches = ( vmLikeEntity: VMLikeEntityKind, @@ -34,3 +186,52 @@ export const getUpdateDescriptionPatches = ( } return patches; }; + +export const getUpdateFlavorPatches = ( + vmLike: VMLikeEntityKind, + templates: TemplateKind[], + flavor: string, + cpu?: number, + mem?: string, +): Patch[] => { + const patches = []; + + // TODO: vm.kubevirt.io/template label should be changed as well (vm.kubevirt.io/template: win2k12r2-server-large) + // TODO: by changing flavor and so the base template, VM devices can be changed as well and so "full delta" should be applied here. + // Considering recent kubevirt state, updating cpu and sockets is good enough for now, but should be enhanced in the future. + + let template; + if (isVM(vmLike)) { + template = getTemplateForFlavor(templates, vmLike as VMKind, flavor); + } else { + const vm = selectVM(vmLike as TemplateKind); + template = getTemplateForFlavor(templates, vm, flavor); + } + + // flavor is set on object.metadata.label level for both VM and VMTemplate + patches.push(...getUpdateFlavorPatch(vmLike, flavor)); + + let customCpu = { + sockets: 1, + cores: cpu, + threads: 1, + }; + let customMem = mem; + if (flavor !== CUSTOM_FLAVOR) { + const templateVm = selectVM(template); + customCpu = parseCPU(getCPU(templateVm), DEFAULT_CPU); + customMem = getMemory(templateVm); + } + + let cpuMemPatches; + if (isVM(vmLike)) { + cpuMemPatches = getUpdateCpuMemoryPatch(vmLike as VMKind, customCpu, customMem); + } else { + cpuMemPatches = getVMLikePatches(vmLike, (vm: VMKind) => + getUpdateCpuMemoryPatch(vm, customCpu, customMem), + ); + } + patches.push(...cpuMemPatches); + + return patches; +}; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/selectors.ts index 2941c630067..3df0a1cd357 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/selectors.ts @@ -1,9 +1,6 @@ import * as _ from 'lodash'; -import { K8sKind, K8sResourceKind, TemplateKind } from '@console/internal/module/k8s'; -import { TemplateModel } from '@console/internal/models'; -import { VirtualMachineModel } from '../models'; -import { VMKind, VMLikeEntityKind } from '../types'; -import { selectVM } from './vm-template/selectors'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { VMLikeEntityKind } from '../types'; export const getLabels = (entity: K8sResourceKind, defaultValue?: any) => _.get(entity, 'metadata.labels', defaultValue) as K8sResourceKind['metadata']['labels']; @@ -14,23 +11,6 @@ export const getDescription = (vm: VMLikeEntityKind) => export const getStorageSize = (value): string => _.get(value, 'requests.storage'); -export const isVM = (vmLikeEntity: VMLikeEntityKind): boolean => - vmLikeEntity && vmLikeEntity.kind === VirtualMachineModel.kind; - -export const getVMLikeModel = (vmLikeEntity: VMLikeEntityKind): K8sKind => - isVM(vmLikeEntity) ? VirtualMachineModel : TemplateModel; - -export const asVM = (vmLikeEntity: VMLikeEntityKind): VMKind => { - if (!vmLikeEntity) { - return null; - } - - if (isVM(vmLikeEntity)) { - return vmLikeEntity as VMKind; - } - return selectVM(vmLikeEntity as TemplateKind); -}; - export const getValueByPrefix = (obj = {}, keyPrefix: string): string => { const objectKey = Object.keys(obj).find((key) => key.startsWith(keyPrefix)); return objectKey ? obj[objectKey] : null; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm-template/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm-template/selectors.ts index 1daae3c1347..1218010361c 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm-template/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm-template/selectors.ts @@ -1,7 +1,104 @@ import * as _ from 'lodash'; +import { getVmTemplate } from 'kubevirt-web-ui-components'; import { TemplateKind } from '@console/internal/module/k8s'; import { VirtualMachineModel } from '../../models'; -import { VMKind } from '../../types'; +import { VMKind, VMLikeEntityKind } from '../../types'; +import { + TEMPLATE_FLAVOR_LABEL, + TEMPLATE_OS_LABEL, + TEMPLATE_WORKLOAD_LABEL, + CUSTOM_FLAVOR, +} from '../../constants'; +import { getLabels } from '../selectors'; +import { getOperatingSystem, getWorkloadProfile } from '../vm/selectors'; export const selectVM = (vmTemplate: TemplateKind): VMKind => _.get(vmTemplate, 'objects', []).find((obj) => obj.kind === VirtualMachineModel.kind); + +export const getTemplatesLabelValues = (templates: TemplateKind[], label: string) => { + const labelValues = []; + (templates || []).forEach((t) => { + const labels = Object.keys(getLabels(t, [])).filter((l) => l.startsWith(label)); + labels.forEach((l) => { + const labelParts = l.split('/'); + if (labelParts.length > 1) { + const labelName = labelParts[labelParts.length - 1]; + if (labelValues.indexOf(labelName) === -1) { + labelValues.push(labelName); + } + } + }); + }); + return labelValues; +}; + +export const getTemplateFlavors = (vmTemplates: TemplateKind[]) => + getTemplatesLabelValues(vmTemplates, TEMPLATE_FLAVOR_LABEL); +export const getTemplateOS = (vmTemplates: TemplateKind[]) => + getTemplatesLabelValues(vmTemplates, TEMPLATE_OS_LABEL); +export const getTemplateWorkloads = (vmTemplates: TemplateKind[]) => + getTemplatesLabelValues(vmTemplates, TEMPLATE_WORKLOAD_LABEL); + +export const getTemplates = ( + templates: TemplateKind[] = [], + os: string, + workload: string, + flavor: string, +) => + templates.filter((t) => { + if (os) { + const templateOS = getTemplateOS([t]); + if (!templateOS.includes(os)) { + return false; + } + } + + if (workload) { + const templateWorkloads = getTemplateWorkloads([t]); + if (!templateWorkloads.includes(workload)) { + return false; + } + } + + if (flavor) { + const templateFlavors = getTemplateFlavors([t]); + if (!templateFlavors.includes(flavor)) { + return false; + } + } + + return true; + }); + +export const getTemplateForFlavor = (templates: TemplateKind[], vm: VMKind, flavor: string) => { + const vmOS = getOperatingSystem(vm); + const vmWorkload = getWorkloadProfile(vm); + const matchingTemplates = getTemplates(templates, vmOS, vmWorkload, flavor); + + // Take first matching. If OS/Workloads changes in the future, there will be another patch sent + return matchingTemplates.length > 0 ? matchingTemplates[0] : undefined; +}; + +export const getFlavors = (vm: VMLikeEntityKind, templates: TemplateKind[]) => { + const vmTemplate = getVmTemplate(vm); + + const flavors = { + // always listed + [CUSTOM_FLAVOR]: CUSTOM_FLAVOR, + }; + + if (vmTemplate) { + // enforced by the vm + const templateFlavors = getTemplateFlavors([vmTemplate]); + templateFlavors.forEach((f) => (flavors[f] = _.capitalize(f))); + } + + // if VM OS or Workload is set, add flavors of matching templates only. Otherwise list all flavors. + const vmOS = getOperatingSystem(vm); + const vmWorkload = getWorkloadProfile(vm); + const matchingTemplates = getTemplates(templates, vmOS, vmWorkload, undefined); + const templateFlavors = getTemplateFlavors(matchingTemplates); + templateFlavors.forEach((f) => (flavors[f] = _.capitalize(f))); + + return flavors; +}; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/cpu.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/cpu.ts new file mode 100644 index 00000000000..0e9cd62fba1 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/cpu.ts @@ -0,0 +1,24 @@ +import { CPURaw, CPU } from '../../types'; + +export const DEFAULT_CPU: CPU = { sockets: 1, cores: 1, threads: 1 }; + +export const parseCPU = (sourceCPURaw: CPURaw, defaultValue?: CPU): CPU => { + if (!sourceCPURaw) { + return defaultValue; + } + + if (typeof sourceCPURaw === 'string') { + return { sockets: 1, cores: parseInt(sourceCPURaw as string, 10), threads: 1 }; + } + + return { + sockets: parseInt(sourceCPURaw.sockets, 10) || 1, + cores: parseInt(sourceCPURaw.cores, 10) || 1, + threads: parseInt(sourceCPURaw.threads, 10) || 1, + }; +}; + +export const vCPUCount = (sourceCPURaw: CPURaw): number => { + const cpu = parseCPU(sourceCPURaw, DEFAULT_CPU); + return cpu.sockets * cpu.cores * cpu.threads; +}; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/index.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/index.ts index c6e62cd4557..09310c7891e 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/index.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/index.ts @@ -4,3 +4,5 @@ export * from './disk'; export * from './nic'; export * from './selectors'; export * from './volume'; +export * from './vmlike'; +export * from './cpu'; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts index b546b60a805..8def016c841 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts @@ -8,7 +8,7 @@ import { TEMPLATE_OS_NAME_ANNOTATION, TEMPLATE_WORKLOAD_LABEL, } from '../../constants/vm'; -import { VMKind } from '../../types'; +import { VMKind, VMLikeEntityKind, CPURaw } from '../../types'; import { findKeySuffixValue, getValueByPrefix } from '../utils'; import { getAnnotations, getLabels } from '../selectors'; import { getDiskBus } from './disk'; @@ -17,7 +17,7 @@ import { Network } from './types'; export const getMemory = (vm: VMKind) => _.get(vm, 'spec.template.spec.domain.resources.requests.memory'); -export const getCPU = (vm: VMKind) => _.get(vm, 'spec.template.spec.domain.cpu.cores'); +export const getCPU = (vm: VMKind): CPURaw => _.get(vm, 'spec.template.spec.domain.cpu'); export const getDisks = (vm: VMKind) => _.get(vm, 'spec.template.spec.domain.devices.disks', []); export const getInterfaces = (vm: VMKind) => _.get(vm, 'spec.template.spec.domain.devices.interfaces', []); @@ -26,13 +26,14 @@ export const getNetworks = (vm: VMKind) => _.get(vm, 'spec.template.spec.network export const getVolumes = (vm: VMKind) => _.get(vm, 'spec.template.spec.volumes', []); export const getDataVolumeTemplates = (vm: VMKind) => _.get(vm, 'spec.dataVolumeTemplates', []); -export const getOperatingSystem = (vm: VMKind) => +export const getOperatingSystem = (vm: VMLikeEntityKind) => findKeySuffixValue(getLabels(vm), TEMPLATE_OS_LABEL); export const getOperatingSystemName = (vm: VMKind) => getValueByPrefix(getAnnotations(vm), `${TEMPLATE_OS_NAME_ANNOTATION}/${getOperatingSystem(vm)}`); -export const getWorkloadProfile = (vm: VMKind) => +export const getWorkloadProfile = (vm: VMLikeEntityKind) => findKeySuffixValue(getLabels(vm), TEMPLATE_WORKLOAD_LABEL); -export const getFlavor = (vm: VMKind) => findKeySuffixValue(getLabels(vm), TEMPLATE_FLAVOR_LABEL); +export const getFlavor = (vmLike: VMLikeEntityKind) => + findKeySuffixValue(getLabels(vmLike), TEMPLATE_FLAVOR_LABEL); export const isVMRunning = (value: VMKind) => _.get(value, 'spec.running', false) as VMKind['spec']['running']; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/vmlike.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/vmlike.ts new file mode 100644 index 00000000000..16e0b3267dd --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/vmlike.ts @@ -0,0 +1,19 @@ +import { K8sKind } from '@console/internal/module/k8s'; +import { TemplateModel } from '@console/internal/models'; +import { VMLikeEntityKind, VMKind } from '../../types'; +import { VirtualMachineModel } from '../../models'; +import { selectVM } from '../vm-template/selectors'; + +export const isVM = (vmLikeEntity: VMLikeEntityKind): vmLikeEntity is VMKind => + vmLikeEntity && vmLikeEntity.kind === VirtualMachineModel.kind; + +export const getVMLikeModel = (vmLikeEntity: VMLikeEntityKind): K8sKind => + isVM(vmLikeEntity) ? VirtualMachineModel : TemplateModel; + +export const asVM = (vmLikeEntity: VMLikeEntityKind): VMKind => { + if (!vmLikeEntity) { + return null; + } + + return isVM(vmLikeEntity) ? vmLikeEntity : selectVM(vmLikeEntity); +}; diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/index.ts b/frontend/packages/kubevirt-plugin/src/types/vm/index.ts index 49e6cba4201..980e5bd770b 100644 --- a/frontend/packages/kubevirt-plugin/src/types/vm/index.ts +++ b/frontend/packages/kubevirt-plugin/src/types/vm/index.ts @@ -58,3 +58,17 @@ export type VMKind = { spec: VMSpec; status: VMStatus; } & K8sResourceKind; + +export type CPU = { + sockets: number; + cores: number; + threads: number; +}; + +export type CPURaw = + | { + sockets: string; + cores: string; + threads: string; + } + | string; diff --git a/frontend/packages/kubevirt-plugin/src/utils/index.ts b/frontend/packages/kubevirt-plugin/src/utils/index.ts index 99035e2eaaf..0c48b007e50 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/index.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/index.ts @@ -13,6 +13,7 @@ import { getKind as getOwnerReferenceKind, getName as getOwnerReferenceName, } from '../selectors/owner-reference/selectors'; +import { CPU } from '../types'; export const getBasicID = (entity: A) => `${getNamespace(entity)}-${getName(entity)}`; @@ -70,3 +71,6 @@ export const compareOwnerReference = (obj: OwnerReference, otherObj: OwnerRefere getOwnerReferenceAPIVersion(obj) === getOwnerReferenceAPIVersion(otherObj) && getOwnerReferenceKind(obj) === getOwnerReferenceKind(otherObj) && getOwnerReferenceName(obj) === getOwnerReferenceName(otherObj); + +export const isCPUEqual = (a: CPU, b: CPU) => + a.sockets === b.sockets && a.cores === b.cores && a.threads === b.threads;