Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add memory validations in the VM wizard #3839

Merged

Conversation

irosenzw
Copy link
Contributor

Fixes: CNV-2913

  • Add common-templates validation type
  • Consume memory validations from common-templates
    and use them in the VM wizard
  • VMSettingsValidationConfig's validator
    can have more than one validator function for a single key

Signed-off-by: Ido Rosenzwig irosenzw@redhat.com

@openshift-ci-robot openshift-ci-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Dec 30, 2019
@irosenzw
Copy link
Contributor Author

@yaacov @glekner please review

@openshift-ci-robot openshift-ci-robot added the component/kubevirt Related to kubevirt-plugin label Dec 30, 2019
},
[VMSettingsField.CPU]: {
detectValueChanges: [VMSettingsField.CPU],
validator: asVMSettingsFieldValidator(validatePositiveInteger),
validators: [asVMSettingsFieldValidator(validatePositiveInteger)],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need a cpu validation ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both validations for CPU and memory that check for a positive integer value seems unnecessary, as you cannot type anything that isn't a positive integer.
But I'm not sure what happens when you use a template that has an invalid value.
haven't tried it though, but I think we can address this question in another PR.
@yaacov What do you think ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the template has a vlidation:

{
    "name": "core-limits",
    "path": "jsonpath::.spec.domain.cpu.cores",
    "message": "cpu cores must be limited",
    "rule": "integer",
    "min": 100,
    "max": 800
}

and user enter 5 cpus, who checks that 5 is not valid value, what am I missing here ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@irosenzw we let the user enter any cpu numer they want, I think we should check for cpu outside the range ... is it problematic ?

For example: a "big-cpu-template" have a rule that min cpu is 18, and user entered 6, this is an invalid cpu number, do we check it ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, the PR is for memory validation only at this point. In the future we will cover all the fields in the VM wizard.

Regarding your question, if this is the only validation, it will fail.
but it there is another template that has a validation of :

{
    "name": "core-limits",
    "path": "jsonpath::.spec.domain.cpu.cores",
    "message": "cpu cores must be limited",
    "rule": "integer",
    "min": 2
}

it will pass.

let relevantTemplates;
// Get templates based on OS and WorkloadProfile
if (os && workloadProfile) {
relevantTemplates = (commonTemplates || []).filter(
Copy link
Member

@yaacov yaacov Dec 31, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you try to make "getRelevantTemplates" method and use it here instead to doing it inline, i think it will look nicer ?

const relevantTemplates = (os && workloadProfile) ? 
    getRelevantTemplates(templates, os, profile, flavor) | []

);

// Get the exact template when a flavor is chosen
if (flavor && flavor !== 'Custom') {
Copy link
Member

@yaacov yaacov Dec 31, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you try to add this check to the filter in line 40 ?

... &&
( (flavor === 'Custom' || iGetIn(t, ['metadata', 'labels', `${TEMPLATE_FLAVOR_LABEL}/${flavor}`]) ),
...


// Get all relevant validations for the specific fieldId
const relevantFieldValidations: CommonTemplatesValidation[] = [];
myValidations.forEach((validationArray: CommonTemplatesValidation[]) => {
Copy link
Member

@yaacov yaacov Dec 31, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make a "getValidation" method ( similar to comment about getRelevantTemplates ) ?

may even be in the selector file and not here ?

relevantTemplates = (commonTemplates || []).filter(
(template) =>
iGetIn(template, ['metadata', 'labels', TEMPLATE_TYPE_LABEL]) === 'base' &&
iGetIn(template, ['metadata', 'labels', `${TEMPLATE_OS_LABEL}/${os}`]) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ca you try this pattern in the tests:

( !os || iGetIn(template, ['metadata', 'labels', `${TEMPLATE_OS_LABEL}/${os}`]) )

it makes this metyhod more general, and will handle cases where we want to search only by flavor or onlt by os ...

const memValueBytes = memValueGB * 1024 ** 3;

// filter all the 'min' validation rules
const minValidations = validations.filter((v) => 'min' in v);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like better running each check individually and looking for first success, then finding the min of all validations and then checking once. because it create a pattern of checking rules we can apply for all checks, e.g. on regex:

 validations.once( ... // do one min validation at a time )
 validations.once( ... // do one regex validation at a time )

iGetIn(t, ['metadata', 'labels', `${TEMPLATE_FLAVOR_LABEL}/${flavor}`]),
);
}
const myValidations: CommonTemplatesValidation[][] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my prefix :-)

@irosenzw irosenzw force-pushed the add-validation-feature branch 3 times, most recently from 6a7ffc9 to 6397059 Compare January 2, 2020 12:43
const commonTemplates = iGetLoadedCommonData(state, id, VMWizardProps.commonTemplates);

// Get templates based on OS and WorkloadProfile
if (os && workloadProfile) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have this check in getRelevantTemplates , it will return an empty array [], and then if getFieldValidations return empty array, you will not need this "if" and not the if in line 50

case IntegerValidtionType.max:
return validateMaxRule(ivo, memValueBytes);
default:
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, are you sure, i'm missing somthing ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. It should be false.

);
};

export const getValidationsFromTemplates = (templates): CommonTemplatesValidation[][] => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm missing something here - why we do not flatten the rules ?

for example, if you have two templates:
template1:

[
          {
            "name": "core-limits",
            "path": "jsonpath::.spec.domain.cpu.cores",
            "message": "cpu cores must be limited",
            "min": 1,
            "max": 8
          }
        ]

template2:

[
    {
      "name": "validation-rule-01",
      "valid": "jsonpath::.some.json.path",
      "path”: "jsonpath::.some.json.path[*].leaf",
      "rule”: "integer",
      "message”: ".some.json.path[*].leaf must exists",
      "min”: 1,
      "justWarning": true
    },
    {
      "name": "validation-rule-02",
      "valid": "jsonpath::.another.json.path",
      "path": "jsonpath::.another.json.path.item",
      "rule": "integer",
      "message": "jsonpath::./another.json.path.item must be below a threshold",
      "max": "jsonpath::.yet.another.json.path.defines.the.limit"
    }
  ]

we should end up with a flat 3 rules and not with 2 sets one having 1 rule and the other 2.

Copy link
Member

@atiratree atiratree Jan 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple templates can occur when for example the workload profile is not selected right? Only one template will be selected at the end.
I think we should pick the most relaxed validation (meaning allowing everything if one template is missing the validation. Or combining min max of the other ones to get the largest interval span).

We should also probably use the validation from the user template if one is selected.

Copy link
Contributor Author

@irosenzw irosenzw Jan 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, the user must pick the OS and the workload profile in order to get the relevant templates from common templates. Only the flavor can be selected as Custom.

Second, when a user pick a flavor, we don't do any validations to the memory - since it is hard-coded in the template. when the user chooses Custom we are getting multiple templates, hence multiple validations from each template.
The idea here is to PASS if at least one of the validations had passed, and FAIL if none had.

user template isn't in the scope of this feature and will/should be addressed in another PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user template isn't in the scope of this feature and will/should be addressed in another PR.

are you sure ? IFAIU it's the simplest test, where you have one template and do not need to look for any other ...

getRelevantTemplates = customTemplate ? [ customTemplate ] | ....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've Added support for user template validations

}
if (validationAttrs.includes('min')) {
if (integerValidationObj.type === IntegerValidtionType.max) {
// change the type to 'between' because we have two seperate validations of min and max
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, when calling this method we should have only one rule - see comment https://github.com/openshift/console/pull/3839/files#r362532470

Copy link
Contributor Author

@irosenzw irosenzw Jan 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessarily.
Most of the times we will have just one, it will be one of the three:

  1. {min:1},{cpu:2},....
  2. {max:10},{cpu:2},...
  3. {min:1,max:10},{cpu:2},....
    but we have here an edge case when a between validation can be written as a 2 separated validations in a single template:
  4. {min:1},{max:10},{cpu:2},....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method should be called with one rule at a time, you should check why it is called with an array ...

// eslint-disable-next-line consistent-return
validations.forEach((validation) => {
const validationAttrs = getValidationAttributes(validation);
if (validationAttrs.includes('min') && validationAttrs.includes('max')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we check the object directly without getting the validationAttrs ?
e.g. is their a way to ask if validation itself has an attribute without first getting all the attributes into a list ... ?

getValidationAttributes looks like something that we should get for free from the language ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I'll remove the method

return getFieldValidations(validations, IDToJsonPath[fieldId]);
};

export const getIntegerValidationType = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method should get one CommonTemplatesValidation and return an enum, we are doing something wrong here ...
I think it's because we didn't flatten the rules in https://github.com/openshift/console/pull/3839/files#r362532470

errorMsg = ivo.message;
switch (ivo.type) {
case IntegerValidtionType.between:
return validateBetweenRule(ivo, memValueBytes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sinse we know that the current rule vArr ( should be one not an array ) is a "between" rule, then vArr must have a min and max properties, we should be able to write: memValueBytes >= vArr.min && vArr.max <= memValueBytes, what am I missing ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented above. it should be an array.

@@ -119,24 +178,28 @@ const validationConfig: VMSettingsValidationConfig = {
]
: [VMWizardProps.activeNamespace, VMWizardProps.virtualMachines];
},
validator: validateVm,
validators: [validateVm],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's not complicate it with an array of validators. Semantically one fields needs only one validation call. The validation can branch inside the call depending on the use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to have multiple validators like this. It is clearer that a field has multiple validators rather than a validator that may or may not branch into multiple validators inside it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it better?
It is unclear what the execution of the validators should be e.g. the order and the validation message.

IMO the interface should be expressive enough for future changes (possible cross validation).

updateAcc[validationFieldKey] = { validation };
}

// break when one validation is invalid
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can keep the old code here if we use one validator and decide the validation priority inside the validator.

return { type: IntegerValidtionType.between, value, message };
};

export const validateMinRule = (ivo: IntegerValidationObj, value: number): boolean => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need all of these wrappers.
I think we could rather do one loop and a set two variables according to the min/max/between

let min = 0;
let max = Number.POSITIVE_INFINITY;

loop over attributes and set accordingly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey, my 2 cents are that we do not need this set of methods at all, instead of calling them, we shoud do:

rule.min >= value 

all this checks are already valid after we check getType(rule) becase if type is minType rule must have a rule.min attribute ...

IntegerBetween,
} from './validation-types';

export const getRelevantTemplates = (commonTemplates, os, workloadProfile, flavor) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this to vm-template selectors?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can. no problem.

);
};

export const getValidationsFromTemplates = (templates): CommonTemplatesValidation[][] => {
Copy link
Member

@atiratree atiratree Jan 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple templates can occur when for example the workload profile is not selected right? Only one template will be selected at the end.
I think we should pick the most relaxed validation (meaning allowing everything if one template is missing the validation. Or combining min max of the other ones to get the largest interval span).

We should also probably use the validation from the user template if one is selected.

// eslint-disable-next-line consistent-return
validations.forEach((validation) => {
const validationAttrs = getValidationAttributes(validation);
if (validationAttrs.includes('min') && validationAttrs.includes('max')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

};

export const runValidation = (validation: CommonTemplatesValidation, value: number): boolean => {
if ('integerBetween' in validation && validation.integerBetween === true) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do

if (('min' in validation) && ('max' in validation))

instead ?

import { CommonTemplatesValidation } from './validation-types';

export const getValidationsFromTemplates = (templates): CommonTemplatesValidation[][] => {
return templates.map((relevantTemplate) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do ?

return templates.map((relevantTemplate) => 
    JSON.parse(iGetIn(relevantTemplate, ['metadata', 'annotations', 'validations'])),
).flat();

message: string; // User-friendly string message describing the failure, should the rule not be satisfied
min?: number; // For 'integer' rule
max?: number; // For 'integer' rule
integerBetween?: boolean; // For 'integer' rule
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

integerBetween?: boolean; // For 'integer' rule
minLength?: number; // For 'string' rule
maxLength?: number; // For 'string' rule
stringBetween?: boolean; // For 'string' rule
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

): CommonTemplatesValidation[] => {
const allTemplateValidations: CommonTemplatesValidation[] = [];
validations.forEach((validationArray: CommonTemplatesValidation[]) => {
validationArray.forEach((validation: CommonTemplatesValidation) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};

export const getFieldValidations = (
validations: CommonTemplatesValidation[][],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validations should be flat ... validations: CommonTemplatesValidation[]

return fieldValidations;
};

export const runValidation = (validation: CommonTemplatesValidation, value: number): boolean => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do ?

export const runValidations = (validations: CommonTemplatesValidation[], value: number): (valid: boolean, mesage: string) => {
   let message = null
    const valid = validations.some((validation) => {
          message = validation.message
           ...
            if ('max' in validation) {
                 return value <= validation.max;
            }
           return false
      }
      ...
      return { valid, messge}
  });

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry the signiture should be like in:
see: https://github.com/openshift/console/pull/3839/files#r362222830

const runVlidations = (validations, value: any) => ...

because this method can accept any type of falue number of string and run the appropriate validation rules.

There is no value that would satisfy both validations.
*/
let errorMsg;
const isValid = validations.some((validation) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do ?

 const ( isValid, message } = runValidations(validations, memValueBytes);

see https://github.com/openshift/console/pull/3839/files#r363104852

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we could create a Validation class which would encapsulate all of it - validation data (on initialization) and the logic

Fixes: CNV-2913

 - Add common-templates validation type
 - Consume memory validations from the user template if chosen
   or from common templates based on OS, workload Profile and flavor fields
   and use them in the VM wizard.

Signed-off-by: Ido Rosenzwig <irosenzw@redhat.com>
workloadProfile: string,
flavor: string,
) => {
if (!commonTemplates || commonTemplates.length === 0 || !os || !workloadProfile) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think it's nice to allow the logic of "if os is not defined => allow all possible os"
so in my view we should do:

 if (!commonTemplates || commonTemplates.length === 0) {

and let the checks in L150 and L151 implement the above logic.

.map((relevantTemplate) =>
JSON.parse(iGetIn(relevantTemplate, ['metadata', 'annotations', 'validations'])),
)
.valueSeq()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what does .valueSeq().toArray() adds here ? does it work if we remove them ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it gets the validations as an array from the immutable object. Thus cannot be removed.

// Get userTemplate if it was chosen
const userTemplateName = iGetVmSettingValue(state, id, VMSettingsField.USER_TEMPLATE);
const iUserTemplates = iGetLoadedCommonData(state, id, VMWizardProps.userTemplates);
const iUserTemplate =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: if we have iUserTemplate we do not need to fetch os, flavor, workloadProfile and commonTemplates

so if we move lines 41...46 above line 32, we can do:

if (iUserTemplate) {
  validations = getValidationsFromTemplates([iUserTemplate]);
  return getFieldValidations(validations, IDToJsonPath[fieldId]);
}

// Get OS, workload-profile and flavor attributes
const os = iGetVmSettingAttribute(s...

@yaacov
Copy link
Member

yaacov commented Jan 6, 2020

/lgtm
have some nitpicks ...

@openshift-ci-robot openshift-ci-robot added the lgtm Indicates that a PR is ready to be merged. label Jan 6, 2020
@openshift-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: irosenzw, yaacov

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci-robot openshift-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jan 6, 2020
@openshift-bot
Copy link
Contributor

/retest

Please review the full test history for this PR and help us cut down flakes.

1 similar comment
@openshift-bot
Copy link
Contributor

/retest

Please review the full test history for this PR and help us cut down flakes.

@openshift-merge-robot openshift-merge-robot merged commit 4ac553e into openshift:master Jan 6, 2020
yaacov added a commit to yaacov/console that referenced this pull request Jan 7, 2020
yaacov added a commit to yaacov/console that referenced this pull request Jan 7, 2020
yaacov added a commit to yaacov/console that referenced this pull request Jan 7, 2020
@yaacov yaacov mentioned this pull request Jan 7, 2020
yaacov added a commit to yaacov/console that referenced this pull request Jan 8, 2020
yaacov added a commit to yaacov/console that referenced this pull request Jan 8, 2020
yaacov added a commit to yaacov/console that referenced this pull request Jan 8, 2020
openshift-merge-robot added a commit that referenced this pull request Jan 9, 2020
@spadgett spadgett added this to the v4.4 milestone Jan 14, 2020
a2batic pushed a commit to a2batic/console that referenced this pull request Jan 16, 2020
rebeccaalpert pushed a commit to rebeccaalpert/console that referenced this pull request Jan 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. component/kubevirt Related to kubevirt-plugin lgtm Indicates that a PR is ready to be merged. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants