diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx index c450afd93dd..5729e5afd0c 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx @@ -289,16 +289,15 @@ export class CreateVMWizardComponent extends React.Component( - concatImmutableLists( - iGetLoadedData(this.props[VMWizardProps.commonTemplates]), - iGetLoadedData(this.props[VMWizardProps.userTemplates]), - ), - ); + const iUserTemplates = iGetLoadedData(this.props[VMWizardProps.userTemplates]); + const iCommonTemplates = iGetLoadedData(this.props[VMWizardProps.commonTemplates]); let promise; if (isProviderImport) { + const templates = immutableListToShallowJS( + concatImmutableLists(iUserTemplates, iCommonTemplates), + ); const { interOPVMSettings, interOPNetworks, interOPStorages } = await kubevirtInterOP({ vmSettings, networks, @@ -323,7 +322,8 @@ export class CreateVMWizardComponent extends React.Component ({ + payload: { + id, + value, + }, + type: InternalActionType.SetTemplateValidations, + }), }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts index c3ca5240daa..4be13be8af2 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts @@ -251,6 +251,11 @@ export default (state, action: WizardInternalAction) => { [dialogID, 'tabs', VMWizardTab.VM_SETTINGS, 'value'], fromJS(payload.value), ); + case InternalActionType.SetTemplateValidations: + return state.setIn( + [dialogID, 'commonData', 'dataIDReferences', 'templateValidations'], + payload.value, + ); default: break; } diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts index 01d4ad09023..6c273f85b13 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts @@ -85,6 +85,8 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio [VMSettingsField.WORKLOAD_PROFILE]: { value: null }, [VMSettingsField.PROVISION_SOURCE_TYPE]: { value: isProviderImport ? undefined : null }, [VMSettingsField.HOSTNAME]: { value: null }, + [VMSettingsField.CPU]: { value: null }, + [VMSettingsField.MEMORY]: { value: null }, }; // filter out oldTemplates diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts index 663165d969c..7719299b9d3 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts @@ -1,4 +1,5 @@ import { FLAGS } from '@console/shared'; +import { TemplateValidations } from '../../../../../utils/validations/template/template-validations'; import { isWinToolsImage, getVolumeContainerImage } from '../../../../../selectors/vm'; import { hasVmSettingsChanged, @@ -20,6 +21,7 @@ import { ProvisionSource } from '../../../../../constants/vm/provision-source'; import { getProviders } from '../../../provider-definitions'; import { windowsToolsStorage } from '../../initial-state/storage-tab-initial-state'; import { getStorages } from '../../../selectors/selectors'; +import { getTemplateValidations } from '../../validations/utils/templates-validations'; import { prefillVmTemplateUpdater } from './prefill-vm-template-state-update'; export const selectedUserTemplateUpdater = (options: UpdateOptions) => { @@ -144,6 +146,25 @@ export const nativeK8sUpdater = ({ id, dispatch, getState, changedCommonData }: ); }; +export const templateValidationsUpdater = (options: UpdateOptions) => { + const { id, prevState, dispatch, getState } = options; + const state = getState(); + if ( + !hasVmSettingsChanged( + prevState, + state, + id, + VMSettingsField.OPERATING_SYSTEM, + VMSettingsField.FLAVOR, + VMSettingsField.WORKLOAD_PROFILE, + ) + ) { + return; + } + const validations: TemplateValidations[] = getTemplateValidations(options); + dispatch(vmWizardInternalActions[InternalActionType.SetTemplateValidations](id, validations)); +}; + export const updateVmSettingsState = (options: UpdateOptions) => [ ...(iGetCommonData(options.getState(), options.id, VMWizardProps.isProviderImport) @@ -154,6 +175,7 @@ export const updateVmSettingsState = (options: UpdateOptions) => flavorUpdater, osUpdater, nativeK8sUpdater, + templateValidationsUpdater, ].forEach((updater) => { updater && updater(options); }); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts index c5572762892..50044df2661 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts @@ -53,6 +53,7 @@ export enum InternalActionType { SetNetworks = 'KubevirtVMWizardSetNetworks', SetStorages = 'KubevirtVMWizardSetStorages', SetResults = 'KubevirtVMWizardSetResults', + SetTemplateValidations = 'TemplateValidations', } export type WizardInternalAction = { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/common-templates-validations.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/common-templates-validations.ts deleted file mode 100644 index dfebfa94d16..00000000000 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/common-templates-validations.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { TemplateKind } from '@console/internal/module/k8s'; -import { getRelevantTemplates } from '../../../../../selectors/vm-template/selectors'; -import { UpdateOptions } from '../../types'; -import { - iGetVmSettingAttribute, - iGetVmSettingValue, -} from '../../../selectors/immutable/vm-settings'; -import { iGetLoadedCommonData, iGetName } from '../../../selectors/immutable/selectors'; -import { VMSettingsField, VMWizardProps, VMSettingsRenderableField } from '../../../types'; -import { CommonTemplatesValidation } from './validation-types'; -import { getValidationsFromTemplates } from './selectors'; - -// TODO: Add all the fields in the form -// For each field we need to check for validations -const IDToJsonPath = { - [VMSettingsField.MEMORY]: 'jsonpath::.spec.domain.resources.requests.memory', - [VMSettingsField.CPU]: 'jsonpath::.spec.domain.cpu.cores', -}; - -export const getTemplateValidations = ( - options: UpdateOptions, - fieldId: VMSettingsRenderableField, -): CommonTemplatesValidation[] => { - // Proceed if there are no validation for the specific fieldId - if (!(fieldId in IDToJsonPath)) { - return []; - } - const { getState, id } = options; - const state = getState(); - - // Get userTemplate if it was chosen - const userTemplateName = iGetVmSettingValue(state, id, VMSettingsField.USER_TEMPLATE); - const iUserTemplates = iGetLoadedCommonData(state, id, VMWizardProps.userTemplates); - const iUserTemplate = - userTemplateName && iUserTemplates - ? iUserTemplates.find((template) => iGetName(template) === userTemplateName) - : null; - - if (iUserTemplate) { - return getValidationsFromTemplates([iUserTemplate] as TemplateKind[], IDToJsonPath[fieldId]); - } - - // Get OS, workload-profile and flavor attributes - const os = iGetVmSettingAttribute(state, id, VMSettingsField.OPERATING_SYSTEM); - const flavor = iGetVmSettingAttribute(state, id, VMSettingsField.FLAVOR); - const workloadProfile = iGetVmSettingAttribute(state, id, VMSettingsField.WORKLOAD_PROFILE); - - // Get all the templates from common-templates - const commonTemplates = iGetLoadedCommonData(state, id, VMWizardProps.commonTemplates).toArray(); - - // Get all the validations from the relevant templates: - // Get the validations from the user template, if chosen. - // If not, get the validations from Common-Templates based on OS, Workload-Profile and Flavor - const templates = getRelevantTemplates(commonTemplates, os, workloadProfile, flavor); - return getValidationsFromTemplates(templates, IDToJsonPath[fieldId]); -}; - -export const runValidation = ( - /* - - Check if at least one validation has passed. - - Would *NOT* check if all the validations have passed since it may not be possible - For example: - If we have two 'between' validations: 2-4 and 6-10 from 2 different templates, - There is no value that would satisfy both validations. - */ - validations: CommonTemplatesValidation[], - value: any, -): { isValid: boolean; errorMsg: string } => { - let errorMsg = null; - const isValid = validations.some((validation) => { - errorMsg = validation.message; - if ('min' in validation && 'max' in validation) { - return value <= validation.max && value >= validation.min; - } - if ('min' in validation) { - return value >= validation.min; - } - if ('max' in validation) { - return value <= validation.max; - } - return false; - }); - - return { isValid, errorMsg }; -}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/selectors.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/selectors.ts deleted file mode 100644 index 615274dd18b..00000000000 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/selectors.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as _ from 'lodash'; -import { TemplateKind } from '@console/internal/module/k8s'; -import { iGetIn } from '../../../../../utils/immutable'; -import { CommonTemplatesValidation } from './validation-types'; - -export const getValidationsFromTemplates = ( - templates: TemplateKind[], - jsonPath: string, -): CommonTemplatesValidation[] => { - const templateValidations: CommonTemplatesValidation[][] = templates.map((relevantTemplate) => - JSON.parse(iGetIn(relevantTemplate, ['metadata', 'annotations', 'validations'])).filter( - (validation: CommonTemplatesValidation) => validation.path.includes(jsonPath), - ), - ); - - // If we have a template with no restrictions, ignore all other validation rules, the most - // relax option take - if (templateValidations.find((validations) => validations.length === 0)) { - return []; - } - - return _.flatten(templateValidations); -}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils/templates-validations.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils/templates-validations.ts new file mode 100644 index 00000000000..b1dfac24b03 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils/templates-validations.ts @@ -0,0 +1,42 @@ +import { UpdateOptions } from '../../types'; +import { + iGetVmSettingAttribute, + iGetVmSettingValue, +} from '../../../selectors/immutable/vm-settings'; +import { iGetLoadedCommonData } from '../../../selectors/immutable/selectors'; +import { VMSettingsField, VMWizardProps } from '../../../types'; +import { iGetTemplateValidations } from '../../../../../selectors/immutable/template/selectors'; +import { iGetRelevantTemplates } from '../../../../../selectors/immutable/template/combined'; +import { TemplateValidations } from '../../../../../utils/validations/template/template-validations'; + +const getValidationsFromTemplates = (templates): TemplateValidations[] => + templates.map( + (relevantTemplate) => new TemplateValidations(iGetTemplateValidations(relevantTemplate)), + ); + +export const getTemplateValidations = (options: UpdateOptions): TemplateValidations[] => { + const { getState, id } = options; + const state = getState(); + + const userTemplateName = iGetVmSettingValue(state, id, VMSettingsField.USER_TEMPLATE); + const os = iGetVmSettingAttribute(state, id, VMSettingsField.OPERATING_SYSTEM); + const flavor = iGetVmSettingAttribute(state, id, VMSettingsField.FLAVOR); + const workload = iGetVmSettingAttribute(state, id, VMSettingsField.WORKLOAD_PROFILE); + + const iUserTemplates = iGetLoadedCommonData(state, id, VMWizardProps.userTemplates); + const iCommonTemplates = iGetLoadedCommonData(state, id, VMWizardProps.commonTemplates); + + const templates = iGetRelevantTemplates(iUserTemplates, iCommonTemplates, { + userTemplateName, + os, + workload, + flavor, + }); + + if (templates.size > 0 && os && workload) { + // templates are sorted by relevance if only flavor is missing + return getValidationsFromTemplates(templates.toArray()); + } + + return getValidationsFromTemplates([templates.first()]); +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts index 1cea07000bb..10a30bcf952 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts @@ -41,11 +41,10 @@ import { import { validatePositiveInteger } from '../../../../utils/validations/common'; import { pluralize } from '../../../../utils/strings'; import { vmSettingsOrder } from '../initial-state/vm-settings-tab-initial-state'; +import { TemplateValidations } from '../../../../utils/validations/template/template-validations'; +import { combineIntegerValidationResults } from '../../../../utils/validations/template/utils'; import { getValidationUpdate } from './utils'; -import { - getTemplateValidations, - runValidation, -} from './common-template-validations/common-templates-validations'; +import { getTemplateValidations } from './utils/templates-validations'; const validateVm: VmSettingsValidator = (field, options) => { const { getState, id } = options; @@ -101,32 +100,22 @@ export const validateOperatingSystem: VmSettingsValidator = (field) => { const memoryValidation: VmSettingsValidator = (field, options): ValidationObject => { const memValueGB = iGetFieldValue(field); - if (!memValueGB) { + if (memValueGB == null || memValueGB === '') { return null; } const memValueBytes = memValueGB * 1024 ** 3; - const validations = getTemplateValidations(options, VMSettingsField.MEMORY); + const validations = getTemplateValidations(options); if (validations.length === 0) { - return null; + validations.push(new TemplateValidations()); // add empty validation for positive integer if it is missing one } - const validationResult = runValidation(validations, memValueBytes); - - if (!validationResult.isValid) { - // Must have failed all validations, including first one: - const validation = validations[0]; - let customMessage = validationResult.errorMsg; - - if ('min' in validation && 'max' in validation) { - customMessage = `Memory must be between ${validation.min / 1024 ** 3}GB and ${validation.max / - 1024 ** 3} GB`; - } else if ('min' in validation) { - customMessage = `Memory must be above ${validation.min / 1024 ** 3}GB`; - } else if ('max' in validation) { - customMessage = `Memory must be below ${validation.max / 1024 ** 3}GB`; - } + const validationResults = validations + .map((v) => v.validateMemory(memValueBytes)) + .filter(({ isValid }) => !isValid); - return asValidationObject(customMessage, ValidationErrorType.Error); + if (validationResults.length === validations.length) { + // every template failed its validations - we cannot choose one + return combineIntegerValidationResults(validationResults, 0); } return null; @@ -177,6 +166,7 @@ const validationConfig: VMSettingsValidationConfig = { VMSettingsField.OPERATING_SYSTEM, VMSettingsField.WORKLOAD_PROFILE, ], + detectCommonDataChanges: [VMWizardProps.userTemplates, VMWizardProps.commonTemplates], validator: memoryValidation, }, }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts index 39d0ed81446..6793714c002 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts @@ -1,5 +1,6 @@ import { get } from 'lodash'; import { Map } from 'immutable'; +import { TemplateValidations } from 'packages/kubevirt-plugin/src/utils/validations/template/template-validations'; import { iGetIn, immutableListToShallowJS } from '../../../utils/immutable'; import { VMWizardNetwork, @@ -50,3 +51,6 @@ export const getStoragesWithWrappers = (state, id: string): VMWizardStorageWithW persistentVolumeClaim, ...rest, })); + +export const getTemplateValidations = (state, id: string): TemplateValidations[] => + iGetIn(getCreateVMWizards(state), [id, 'commonData', 'dataIDReferences', 'templateValidations']); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts index 449d6a2595b..478d00e9a14 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts @@ -24,7 +24,7 @@ export const titleResolver: VMSettingsRenderableFieldResolver = { [VMSettingsField.IMAGE_URL]: 'URL', [VMSettingsField.OPERATING_SYSTEM]: 'Operating System', [VMSettingsField.FLAVOR]: 'Flavor', - [VMSettingsField.MEMORY]: 'Memory (GB)', + [VMSettingsField.MEMORY]: 'Memory (GiB)', [VMSettingsField.CPU]: 'CPUs', [VMSettingsField.WORKLOAD_PROFILE]: 'Workload Profile', [VMSettingsField.START_VM]: 'Start virtual machine on creation', diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-modal-enhanced.tsx index 692b36610b3..60cae4b4856 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-modal-enhanced.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-modal-enhanced.tsx @@ -8,6 +8,7 @@ import { ProjectModel, StorageClassModel, } from '@console/internal/models'; +import { TemplateValidations } from 'packages/kubevirt-plugin/src/utils/validations/template/template-validations'; import { iGetCommonData } from '../../selectors/immutable/selectors'; import { VMWizardProps, @@ -17,7 +18,7 @@ import { } from '../../types'; import { vmWizardActions } from '../../redux/actions'; import { ActionType } from '../../redux/types'; -import { getStoragesWithWrappers } from '../../selectors/selectors'; +import { getStoragesWithWrappers, getTemplateValidations } from '../../selectors/selectors'; import { DiskWrapper } from '../../../../k8s/wrapper/vm/disk-wrapper'; import { VolumeWrapper } from '../../../../k8s/wrapper/vm/volume-wrapper'; import { DataVolumeWrapper } from '../../../../k8s/wrapper/vm/data-volume-wrapper'; @@ -34,6 +35,7 @@ const VMWizardStorageModal: React.FC = (props) => { useProjects, addUpdateStorage, storages, + templateValidations, ...restProps } = props; const { @@ -80,6 +82,17 @@ const VMWizardStorageModal: React.FC = (props) => { }, ]; + const getAllowedBusses = (): Set => { + // Empty Set means all values are excepted + return new Set( + templateValidations.reduce( + (result: string[], tv: TemplateValidations) => + result.concat(Array.from(tv.getAllowedBusses())), + [], + ), + ); + }; + return ( = (props) => { ].includes(type)} isCreateTemplate={isCreateTemplate} isEditing={isEditing} + allowedBusses={getAllowedBusses()} onSubmit={( resultDiskWrapper, resultVolumeWrapper, @@ -138,6 +152,7 @@ type VMWizardStorageModalProps = ModalComponentProps & { useProjects?: boolean; isCreateTemplate: boolean; storages: VMWizardStorageWithWrappers[]; + templateValidations: TemplateValidations[]; addUpdateStorage: (storage: VMWizardStorage) => void; }; @@ -148,6 +163,7 @@ const stateToProps = (state, { wizardReduxID }) => { namespace: iGetCommonData(state, wizardReduxID, VMWizardProps.activeNamespace), isCreateTemplate: iGetCommonData(state, wizardReduxID, VMWizardProps.isCreateTemplate), storages: getStoragesWithWrappers(state, wizardReduxID), + templateValidations: getTemplateValidations(state, wizardReduxID), }; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx index bad941460e5..decdd8caab4 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx @@ -11,7 +11,7 @@ import { StorageClassModel, } from '@console/internal/models'; import { getLoadedData } from '../../../utils'; -import { asVM, getVMLikeModel } from '../../../selectors/vm'; +import { asVM, getVMLikeModel, getOperatingSystem } from '../../../selectors/vm'; import { VMLikeEntityKind } from '../../../types'; import { DiskWrapper } from '../../../k8s/wrapper/vm/disk-wrapper'; import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; @@ -26,6 +26,9 @@ const DiskModalFirehoseComponent: React.FC = (p const vmLikeFinal = getLoadedData(vmLikeEntityLoading, vmLikeEntity); // default old snapshot before loading a new one const vm = asVM(vmLikeFinal); + const allowedBusses = getOperatingSystem(vmLikeEntity).startsWith('win') + ? new Set(['virtio', 'sata']) + : new Set(); const diskWrapper = disk ? DiskWrapper.initialize(disk) : DiskWrapper.EMPTY; const volumeWrapper = volume ? VolumeWrapper.initialize(volume) : VolumeWrapper.EMPTY; const dataVolumeWrapper = dataVolume @@ -61,6 +64,7 @@ const DiskModalFirehoseComponent: React.FC = (p volume={volumeWrapper} dataVolume={dataVolumeWrapper} onSubmit={onSubmit} + allowedBusses={allowedBusses} /> ); }; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal.tsx index bf15d7e716b..6fa42882253 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal.tsx @@ -69,6 +69,7 @@ export const DiskModal = withHandlePromise((props: DiskModalProps) => { handlePromise, close, cancel, + allowedBusses, } = props; const asId = prefixedID.bind(null, 'disk'); const disk = props.disk || DiskWrapper.EMPTY; @@ -380,9 +381,24 @@ export const DiskModal = withHandlePromise((props: DiskModalProps) => { isDisabled={inProgress} > - {DiskBus.getAll().map((b) => ( - - ))} + {!allowedBusses || allowedBusses.size === 0 + ? DiskBus.getAll().map((b) => ( + + )) + : DiskBus.getAll().map( + (b) => + Array.from(allowedBusses).includes(b.getValue()) && ( + + ), + )} {source.requiresStorageClass() && ( @@ -443,6 +459,7 @@ export type DiskModalProps = { vmName: string; vmNamespace: string; namespace: string; + allowedBusses?: Set; onNamespaceChanged: (namespace: string) => void; usedDiskNames: Set; usedPVCNames: Set; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/common.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/common.ts index ed3268ce930..ed812192891 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/common.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/common.ts @@ -20,18 +20,20 @@ import { import { MutableVMWrapper } from '../../../wrapper/vm/vm-wrapper'; import { getTemplateOperatingSystems } from '../../../../selectors/vm-template/advanced'; import { MutableVMTemplateWrapper } from '../../../wrapper/vm/vm-template-wrapper'; -import { operatingSystemsNative } from '../../../../components/create-vm-wizard/native/consts'; +import { concatImmutableLists, immutableListToShallowJS } from '../../../../utils/immutable'; import { CreateVMEnhancedParams } from './types'; export const initializeCommonMetadata = ( - { vmSettings, templates, openshiftFlag }: CreateVMEnhancedParams, + { vmSettings, iUserTemplates, iCommonTemplates }: CreateVMEnhancedParams, entity: MutableVMWrapper | MutableVMTemplateWrapper, template?: TemplateKind, ) => { const settings = asSimpleSettings(vmSettings); - const operatingSystems = openshiftFlag - ? getTemplateOperatingSystems(templates) - : operatingSystemsNative; + const templates = immutableListToShallowJS( + concatImmutableLists(iUserTemplates, iCommonTemplates), + ); + + const operatingSystems = getTemplateOperatingSystems(templates); const osID = settings[VMSettingsField.OPERATING_SYSTEM]; const osName = (operatingSystems.find(({ id }) => id === osID) || {}).name; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/create.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/create.ts index 26b1ecfed76..8c5897a9d3a 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/create.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/create.ts @@ -21,21 +21,24 @@ import { buildOwnerReference } from '../../../../utils'; import { selectVM } from '../../../../selectors/vm-template/selectors'; import { MutableVMWrapper } from '../../../wrapper/vm/vm-wrapper'; import { ProcessedTemplatesModel } from '../../../../models/models'; -import { findTemplate } from './selectors'; +import { toShallowJS } from '../../../../utils/immutable'; +import { iGetRelevantTemplate } from '../../../../selectors/immutable/template/combined'; import { CreateVMEnhancedParams, CreateVMParams } from './types'; import { initializeVM } from './initialize-vm'; import { initializeCommonMetadata, initializeCommonVMMetadata } from './common'; export const getInitializedVMTemplate = (params: CreateVMEnhancedParams) => { - const { vmSettings, templates } = params; + const { vmSettings, iCommonTemplates, iUserTemplates } = params; const settings = asSimpleSettings(vmSettings); - const temp = findTemplate(templates, { - userTemplateName: settings[VMSettingsField.USER_TEMPLATE], - workload: settings[VMSettingsField.WORKLOAD_PROFILE], - flavor: settings[VMSettingsField.FLAVOR], - os: settings[VMSettingsField.OPERATING_SYSTEM], - }); + const temp = toShallowJS( + iGetRelevantTemplate(iUserTemplates, iCommonTemplates, { + userTemplateName: settings[VMSettingsField.USER_TEMPLATE], + workload: settings[VMSettingsField.WORKLOAD_PROFILE], + flavor: settings[VMSettingsField.FLAVOR], + os: settings[VMSettingsField.OPERATING_SYSTEM], + }), + ); if (!temp) { return {}; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/selectors.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/selectors.ts deleted file mode 100644 index 6851a19ff81..00000000000 --- a/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/selectors.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getName } from '@console/shared/src'; -import { TemplateKind } from '@console/internal/module/k8s'; -import { - getTemplatesOfLabelType, - getTemplatesWithLabels, -} from '../../../../selectors/vm-template/advanced'; -import { - getFlavorLabel, - getOsLabel, - getWorkloadLabel, -} from '../../../../selectors/vm-template/combined-dependent'; -import { TEMPLATE_TYPE_BASE } from '../../../../constants/vm'; - -type FindTemplateOptions = { - userTemplateName?: string; - workload?: string; - flavor?: string; - os?: string; -}; - -export const findTemplate = ( - templates: TemplateKind[], - { userTemplateName, workload, flavor, os }: FindTemplateOptions, -): TemplateKind => { - if (userTemplateName) { - return templates.find((template) => getName(template) === userTemplateName); - } - return getTemplatesWithLabels(getTemplatesOfLabelType(templates, TEMPLATE_TYPE_BASE), [ - getOsLabel(os), - getWorkloadLabel(workload), - getFlavorLabel(flavor), - ])[0]; -}; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/types.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/types.ts index 439b9e83928..9f7cf595cbf 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/types.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/types.ts @@ -1,14 +1,17 @@ -import { ConfigMapKind, TemplateKind } from '@console/internal/module/k8s'; +import { ConfigMapKind } from '@console/internal/module/k8s'; +import { Map as ImmutableMap } from 'immutable'; import { EnhancedK8sMethods } from '../../../enhancedK8sMethods/enhancedK8sMethods'; import { VMSettings } from '../../../../components/create-vm-wizard/redux/initial-state/types'; import { VMWizardNetwork, VMWizardStorage } from '../../../../components/create-vm-wizard/types'; +import { ITemplate } from '../../../../types/template'; export type CreateVMParams = { enhancedK8sMethods: EnhancedK8sMethods; vmSettings: VMSettings; networks: VMWizardNetwork[]; storages: VMWizardStorage[]; - templates: TemplateKind[]; + iUserTemplates: ImmutableMap; + iCommonTemplates: ImmutableMap; namespace: string; openshiftFlag: boolean; }; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/vm-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/vm-wrapper.ts index 7a77334251d..bfe7d289cd6 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/vm-wrapper.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/vm-wrapper.ts @@ -96,7 +96,7 @@ export class MutableVMWrapper extends VMWrapper { return this; }; - setMemory = (value: string, unit = 'G') => { + setMemory = (value: string, unit = 'Gi') => { this.ensurePath('spec.template.spec.domain.resources.requests', {}); this.data.spec.template.spec.domain.resources.requests.memory = `${value}${unit}`; return this; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/immutable/common.ts b/frontend/packages/kubevirt-plugin/src/selectors/immutable/common.ts new file mode 100644 index 00000000000..c6bd918d74c --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/selectors/immutable/common.ts @@ -0,0 +1,4 @@ +import { iGetIn } from '../../utils/immutable'; +import { ILabels } from '../../types/template'; + +export const iGetLabels = (obj): ILabels => iGetIn(obj, ['metadata', 'labels']); diff --git a/frontend/packages/kubevirt-plugin/src/selectors/immutable/template/combined.ts b/frontend/packages/kubevirt-plugin/src/selectors/immutable/template/combined.ts new file mode 100644 index 00000000000..da4fb6417a6 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/selectors/immutable/template/combined.ts @@ -0,0 +1,78 @@ +import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; +import { getFlavorLabel, getOsLabel, getWorkloadLabel } from '../../vm-template/combined-dependent'; +import { TEMPLATE_TYPE_BASE, TEMPLATE_TYPE_LABEL } from '../../../constants/vm'; +import { iGetName } from '../../../components/create-vm-wizard/selectors/immutable/selectors'; +import { ITemplate } from '../../../types/template'; +import { iGetLabels } from '../common'; + +type FindTemplateOptions = { + userTemplateName?: string; + workload?: string; + flavor?: string; + os?: string; +}; + +const flavorOrder = { + large: 0, + medium: 1, + small: 2, + tiny: 3, +}; + +export const iGetRelevantTemplates = ( + iUserTemplates: ImmutableMap, + iCommonTemplates: ImmutableMap, + { userTemplateName, workload, flavor, os }: FindTemplateOptions, +): ImmutableList => { + if (userTemplateName && iUserTemplates) { + const relevantTemplate = iUserTemplates.find( + (template) => iGetName(template) === userTemplateName, + ); + return ImmutableList.of(relevantTemplate); + } + + const osLabel = getOsLabel(os); + const workloadLabel = getWorkloadLabel(workload); + const flavorLabel = getFlavorLabel(flavor); + + return ImmutableList( + (iCommonTemplates || ImmutableMap()) + .valueSeq() + .filter((iTemplate) => { + const labels = iGetLabels(iTemplate); + + return ( + labels && + labels.get(TEMPLATE_TYPE_LABEL) === TEMPLATE_TYPE_BASE && + (!osLabel || labels.has(osLabel)) && + (!workloadLabel || labels.has(workloadLabel)) && + (!flavorLabel || labels.has(flavorLabel)) + ); + }) + .sort((a, b) => { + const aLabels = iGetLabels(a); + const bLabels = iGetLabels(b); + + const aFlavor = + aLabels && + flavorOrder[Object.keys(flavorOrder).find((f) => aLabels.has(getFlavorLabel(f)))]; + const bFlavor = + bLabels && + flavorOrder[Object.keys(flavorOrder).find((f) => bLabels.has(getFlavorLabel(f)))]; + + if (aFlavor == null) { + return -1; + } + + if (bFlavor == null) { + return 1; + } + + return aFlavor - bFlavor; + }), + ); +}; + +export const iGetRelevantTemplate = ( + ...args: Parameters +): ITemplate => iGetRelevantTemplates(...args).first(); diff --git a/frontend/packages/kubevirt-plugin/src/selectors/immutable/template/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/immutable/template/selectors.ts new file mode 100644 index 00000000000..fe6ec9d1f83 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/selectors/immutable/template/selectors.ts @@ -0,0 +1,12 @@ +import { iGetIn } from '../../../utils/immutable'; +import { CommonTemplatesValidation, ITemplate } from '../../../types/template'; + +export const iGetTemplateValidations = (template: ITemplate): CommonTemplatesValidation[] => { + const result = iGetIn(template, ['metadata', 'annotations', 'validations']); + + try { + return result ? JSON.parse(result) : []; + } catch (e) { + return []; + } +}; 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 4e269e42b98..d21a1ed2b92 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm-template/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm-template/selectors.ts @@ -140,8 +140,8 @@ export const getRelevantTemplates = ( os: string, workloadProfile: string, flavor: string, -) => - (commonTemplates || []).filter( +) => { + const relevantTemplates = (commonTemplates || []).filter( (template) => iGetIn(template, ['metadata', 'labels', TEMPLATE_TYPE_LABEL]) === 'base' && (!os || iGetIn(template, ['metadata', 'labels', `${TEMPLATE_OS_LABEL}/${os}`])) && @@ -154,3 +154,5 @@ export const getRelevantTemplates = ( (flavor === 'Custom' || iGetIn(template, ['metadata', 'labels', `${TEMPLATE_FLAVOR_LABEL}/${flavor}`])), ); + return relevantTemplates; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/validation-types.ts b/frontend/packages/kubevirt-plugin/src/types/template/index.ts similarity index 86% rename from frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/validation-types.ts rename to frontend/packages/kubevirt-plugin/src/types/template/index.ts index 8a301fb3fa1..8af89d30aa0 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/common-template-validations/validation-types.ts +++ b/frontend/packages/kubevirt-plugin/src/types/template/index.ts @@ -1,3 +1,5 @@ +import { Map } from 'immutable'; + enum commonTemplatesValidationRules { integer = 'integer', string = 'string', @@ -18,3 +20,6 @@ export type CommonTemplatesValidation = { values?: string[]; // For 'enum' rule justWarning?: boolean; }; + +export type ILabels = Map; +export type ITemplate = Map; diff --git a/frontend/packages/kubevirt-plugin/src/utils/immutable.ts b/frontend/packages/kubevirt-plugin/src/utils/immutable.ts index 4aea9840695..7f21bf67b62 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/immutable.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/immutable.ts @@ -1,7 +1,7 @@ import * as _ from 'lodash'; import { List } from 'immutable'; -export const concatImmutableLists = (...args) => +export const concatImmutableLists = (...args): List => args.filter((list) => list).reduce((acc, nextArray) => acc.concat(nextArray), List()); export const iFirehoseResultToJS = (immutableValue, isList = true) => { diff --git a/frontend/packages/kubevirt-plugin/src/utils/strings.ts b/frontend/packages/kubevirt-plugin/src/utils/strings.ts index 4179156a6f6..1cbc4a30347 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/strings.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/strings.ts @@ -33,3 +33,11 @@ export const getSequenceName = (name: string, usedSequenceNames?: Set) = export const pluralize = (i: number, singular: string, plural: string = `${singular}s`) => i === 1 ? singular : plural; + +export const intervalBracket = (isInclusive: boolean, leftValue?: number, rightValue?: number) => { + if (leftValue) { + return isInclusive && Number.isFinite(leftValue) ? '[' : '('; + } + + return isInclusive && Number.isFinite(rightValue) ? ']' : ')'; +}; diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/template/template-validations.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/template/template-validations.ts new file mode 100644 index 00000000000..3f317db184a --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/template/template-validations.ts @@ -0,0 +1,131 @@ +/* eslint-disable lines-between-class-members */ +import * as _ from 'lodash'; +import { humanizeBinaryBytes } from '@console/internal/components/utils'; +import { ValueEnum } from '../../../constants'; +import { CommonTemplatesValidation } from '../../../types/template'; +import { IntegerValidationResult } from './types'; + +// TODO: Add all the fields in the form +export class ValidationJSONPath extends ValueEnum { + static readonly CPU = new ValidationJSONPath('jsonpath::.spec.domain.cpu.cores'); + static readonly MEMORY = new ValidationJSONPath( + 'jsonpath::.spec.domain.resources.requests.memory', + ); + static readonly BUS = new ValidationJSONPath('jsonpath::.spec.domain.devices.disks[*].disk.bus'); +} + +export class TemplateValidations { + private validations: CommonTemplatesValidation[]; + + constructor(validations: CommonTemplatesValidation[] = []) { + this.validations = _.compact(validations); + } + + validateMemory = (value: number): IntegerValidationResult => { + const { min, max, isMinInclusive, isMaxInclusive, isValid } = this.validateInteger( + value, + ValidationJSONPath.MEMORY, + { + defaultMin: 0, + isDefaultMinInclusive: false, + }, + ); + + let errorMsg = null; + + if (!isValid) { + if (min !== 0 && Number.isFinite(min) && Number.isFinite(max)) { + errorMsg = `Memory must be between ${humanizeBinaryBytes(min).string} and ${ + humanizeBinaryBytes(max).string + }`; + } else if (Number.isFinite(max)) { + errorMsg = `Memory must be below ${humanizeBinaryBytes(max).string}`; + } else { + errorMsg = `Memory must be above ${humanizeBinaryBytes(min).string}`; + } + } + return { isValid, errorMsg, min, max, isMinInclusive, isMaxInclusive }; + }; + + private validateInteger = ( + value: number, + jsonPath: ValidationJSONPath, + { + isDefaultMinInclusive = true, + isDefaultMaxInclusive = true, + defaultMin = Number.NEGATIVE_INFINITY, + defaultMax = Number.POSITIVE_INFINITY, + }, + ) => { + const relevantValidations = this.getRelevantValidations(jsonPath); + + // combine validations for single template and make them strict (all integer validations must pass) + const { min, max, isMinInclusive, isMaxInclusive } = relevantValidations.reduce( + ( + { + min: oldMin, + max: oldMax, + isMinInclusive: oldIsMinInclusive, + isMaxInclusive: oldIsMaxInclusive, + }, + validation, + ) => { + let newMin = oldMin; + let newMax = oldMax; + let newIsMinInclusive = oldIsMinInclusive; + let newIsMaxInclusive = oldIsMaxInclusive; + + if ('min' in validation && validation.min >= oldMin) { + newMin = validation.min; + newIsMinInclusive = true; + } + if ('max' in validation && validation.max <= oldMax) { + newMax = validation.max; + newIsMaxInclusive = true; + } + return { + min: newMin, + max: newMax, + isMinInclusive: newIsMinInclusive, + isMaxInclusive: newIsMaxInclusive, + }; + }, + { + min: defaultMin, + max: defaultMax, + isMinInclusive: isDefaultMinInclusive, + isMaxInclusive: isDefaultMaxInclusive, + }, + ); + + const isValid = + (isMinInclusive ? min <= value : min < value) && + (isMaxInclusive ? value <= max : value < max); + + return { min, max, isMinInclusive, isMaxInclusive, isValid }; + }; + + getAllowedBusses = (): Set => this.getAllowedEnumValues(ValidationJSONPath.BUS); + + private getAllowedEnumValues = (jsonPath: ValidationJSONPath): Set => { + // Empty array means all values are allowed + + // Get all the validations which has the 'values' key and aren't optional + const relevantValidations = this.getRelevantValidations(jsonPath).filter( + (validation) => !validation.justWarning && 'values' in validation, + ); + + return new Set( + relevantValidations.reduce( + (result: string[], validation: CommonTemplatesValidation) => + result.concat(validation.values), + [], + ), + ); + }; + + private getRelevantValidations = (jsonPath: ValidationJSONPath) => + this.validations.filter((validation: CommonTemplatesValidation) => + validation.path.includes(jsonPath.getValue()), + ); +} diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/template/types.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/template/types.ts new file mode 100644 index 00000000000..96d875a016e --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/template/types.ts @@ -0,0 +1,8 @@ +export type IntegerValidationResult = { + isValid: boolean; + errorMsg: string; + min?: number; + max?: number; + isMinInclusive?: boolean; + isMaxInclusive?: boolean; +}; diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/template/utils.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/template/utils.ts new file mode 100644 index 00000000000..a8381ded155 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/template/utils.ts @@ -0,0 +1,46 @@ +import * as _ from 'lodash'; +import { + asValidationObject, + joinGrammaticallyListOfItems, + ValidationErrorType, +} from '@console/shared/src'; +import { humanizeBinaryBytes } from '@console/internal/components/utils'; +import { intervalBracket } from '../../strings'; +import { IntegerValidationResult } from './types'; + +const humanize = (value: number) => + Number.isFinite(value) ? humanizeBinaryBytes(value).string : value; + +export const combineIntegerValidationResults = ( + results: IntegerValidationResult[], + defaultMin = Number.NEGATIVE_INFINITY, + defaultMax = Number.POSITIVE_INFINITY, +) => { + if (!results || results.length === 0) { + return null; + } + let message; + if (results.length === 1) { + message = results[0].errorMsg; + } else { + const maxBoundsResult = results.find( + ({ min, max }) => min === defaultMin && max === defaultMax, + ); + if (maxBoundsResult) { + message = maxBoundsResult.errorMsg; + } else { + message = `Memory must be in one of these intervals: ${joinGrammaticallyListOfItems( + _.uniqWith(results, (a, b) => a.min === b.min && a.max === b.max) + .sort((a, b) => a.min - b.min) + .map( + ({ min, max, isMinInclusive, isMaxInclusive }) => + `${intervalBracket(isMinInclusive, min)}${humanize(min)} - ${humanize( + max, + )}${intervalBracket(isMaxInclusive, null, max)}`, + ), + 'or', + )}`; + } + } + return asValidationObject(message, ValidationErrorType.Error); +};