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 { - // 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 57b3a476ff5..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..c1ba668c8ad --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils/templates-validations.ts @@ -0,0 +1,56 @@ +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 { + iGetRelevantTemplate, + 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 relevantOptions = { + userTemplateName, + os, + workload, + flavor, + }; + + const iUserTemplates = iGetLoadedCommonData(state, id, VMWizardProps.userTemplates); + const iCommonTemplates = iGetLoadedCommonData(state, id, VMWizardProps.commonTemplates); + + if (userTemplateName || (flavor && os && workload)) { + // all information is filled to select a final template + const relevantTemplate = iGetRelevantTemplate( + iUserTemplates, + iCommonTemplates, + relevantOptions, + ); + return getValidationsFromTemplates(relevantTemplate ? [relevantTemplate] : []); + } + + const relevantTemplates = iGetRelevantTemplates( + iUserTemplates, + iCommonTemplates, + relevantOptions, + ); + + return getValidationsFromTemplates(relevantTemplates.toArray()); +}; 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..af5c6fea923 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,25 @@ 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, { + defaultMin: 0, + isDefaultMinInclusive: false, + }); } return null; @@ -177,6 +169,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/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/k8s/requests/vm/create/common.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/vm/create/common.ts index ed3268ce930..3578bb9c799 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 @@ -21,16 +21,21 @@ 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, openshiftFlag, iCommonTemplates }: CreateVMEnhancedParams, entity: MutableVMWrapper | MutableVMTemplateWrapper, template?: TemplateKind, ) => { const settings = asSimpleSettings(vmSettings); const operatingSystems = openshiftFlag - ? getTemplateOperatingSystems(templates) + ? getTemplateOperatingSystems( + immutableListToShallowJS( + concatImmutableLists(iUserTemplates, iCommonTemplates), + ), + ) : operatingSystemsNative; 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/interval-validation-result.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/template/interval-validation-result.ts new file mode 100644 index 00000000000..c566ba4e034 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/template/interval-validation-result.ts @@ -0,0 +1,57 @@ +/* eslint-disable lines-between-class-members */ +import { ValidationErrorType } from '@console/shared/src'; +import { humanizeBinaryBytes } from '@console/internal/components/utils'; +import { Interval } from './types'; + +export class IntervalValidationResult implements Interval { + type: ValidationErrorType; + isValid: boolean; + min?: number; + max?: number; + isMinInclusive?: boolean; + isMaxInclusive?: boolean; + + constructor({ + type, + isValid, + min, + max, + isMinInclusive, + isMaxInclusive, + }: Interval & { + isValid: boolean; + type: ValidationErrorType; + }) { + this.type = type; + this.isValid = isValid; + this.min = min; + this.max = max; + this.isMinInclusive = isMinInclusive; + this.isMaxInclusive = isMaxInclusive; + } + + public getErrorMessage = () => (this.isValid ? null : 'Interval is not valid'); +} + +export class MemoryIntervalValidationResult extends IntervalValidationResult { + public getErrorMessage = () => { + const verb = this.type === ValidationErrorType.Warn ? 'should' : 'must'; + + if (!this.isValid) { + if (this.min !== 0 && Number.isFinite(this.min) && Number.isFinite(this.max)) { + return `Memory ${verb} be between ${humanizeBinaryBytes(this.min).string} and ${ + humanizeBinaryBytes(this.max).string + }`; + } + if (Number.isFinite(this.max)) { + return `Memory ${verb} be ${this.isMaxInclusive ? 'at most' : 'bellow'} ${ + humanizeBinaryBytes(this.max).string + }`; + } + return `Memory ${verb} be ${this.isMinInclusive ? 'at least' : 'above'} ${ + humanizeBinaryBytes(this.min).string + }`; + } + return null; + }; +} 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..b648b72e37f --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/template/template-validations.ts @@ -0,0 +1,120 @@ +/* eslint-disable lines-between-class-members */ +import * as _ from 'lodash'; +import { ValidationErrorType } from '@console/shared/src'; +import { ValueEnum } from '../../../constants'; +import { CommonTemplatesValidation } from '../../../types/template'; +import { + IntervalValidationResult, + MemoryIntervalValidationResult, +} from './interval-validation-result'; + +// 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', + ); +} + +export class TemplateValidations { + private validations: CommonTemplatesValidation[]; + + constructor(validations: CommonTemplatesValidation[] = []) { + this.validations = _.compact(validations); + } + + validateMemory = (value: number): IntervalValidationResult => { + const result = this.validateMemoryByType(value, ValidationErrorType.Error); + if (!result.isValid) { + return result; + } + + return this.validateMemoryByType(value, ValidationErrorType.Warn); + }; + + private validateMemoryByType = ( + value: number, + type: ValidationErrorType, + ): IntervalValidationResult => + new MemoryIntervalValidationResult( + this.validateInterval(value, ValidationJSONPath.MEMORY, { + defaultMin: 0, + isDefaultMinInclusive: false, + type, + }), + ); + + private validateInterval = ( + value: number, + jsonPath: ValidationJSONPath, + { + isDefaultMinInclusive = true, + isDefaultMaxInclusive = true, + defaultMin = Number.NEGATIVE_INFINITY, + defaultMax = Number.POSITIVE_INFINITY, + type = ValidationErrorType.Error, + }, + ): IntervalValidationResult => { + const relevantValidations = this.getRelevantValidations(jsonPath, type); + + // 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 new IntervalValidationResult({ + type, + isValid, + min, + max, + isMinInclusive, + isMaxInclusive, + }); + }; + + private getRelevantValidations = (jsonPath: ValidationJSONPath, type: ValidationErrorType) => { + return this.validations.filter( + (validation: CommonTemplatesValidation) => + validation.path.includes(jsonPath.getValue()) && + (type === ValidationErrorType.Warn) === !!validation.justWarning, + ); + }; +} 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..31877533ede --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/template/types.ts @@ -0,0 +1,6 @@ +export interface Interval { + 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..5d81aa0abe6 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/template/utils.ts @@ -0,0 +1,69 @@ +import * as _ from 'lodash'; +import { + asValidationObject, + joinGrammaticallyListOfItems, + ValidationErrorType, +} from '@console/shared/src'; +import { humanizeBinaryBytes } from '@console/internal/components/utils'; +import { intervalBracket } from '../../strings'; +import { Interval } from './types'; +import { IntervalValidationResult } from './interval-validation-result'; + +const humanize = (value: number) => + Number.isFinite(value) ? humanizeBinaryBytes(value).string : value; + +const intervalEquals = (a: Interval, b: Interval) => + a && + b && + a.min === b.min && + a.isMinInclusive === b.isMinInclusive && + a.max === b.max && + a.isMaxInclusive === b.isMaxInclusive; + +export const combineIntegerValidationResults = ( + results: IntervalValidationResult[], + { + isDefaultMinInclusive = true, + isDefaultMaxInclusive = true, + defaultMin = Number.NEGATIVE_INFINITY, + defaultMax = Number.POSITIVE_INFINITY, + }, +) => { + if (!results || results.length === 0) { + return null; + } + const uniqueResults = _.uniqWith(results, (a, b) => intervalEquals(a, b) && a.type === b.type); + // these validations come from different templates so prefer Warn over Error + const hasWarning = uniqueResults.some((r) => r.type === ValidationErrorType.Warn); + const finalType = hasWarning ? ValidationErrorType.Warn : ValidationErrorType.Error; + let message; + if (uniqueResults.length === 1) { + message = uniqueResults[0].getErrorMessage(); + } else { + const defaultInterval = { + min: defaultMin, + max: defaultMax, + isMaxInclusive: isDefaultMaxInclusive, + isMinInclusive: isDefaultMinInclusive, + }; + const maxBoundsResult = uniqueResults.find((r) => intervalEquals(r, defaultInterval)); + if (!hasWarning && maxBoundsResult) { + message = maxBoundsResult.getErrorMessage(); + } else { + const verb = this.type === ValidationErrorType.Warn ? 'should' : 'must'; + // include all types to show all intervals + message = `Memory ${verb} be in one of these intervals: ${joinGrammaticallyListOfItems( + _.uniqWith(results, intervalEquals) + .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, finalType); +};