@codefresh-io/yaml-validator
Version:
An NPM module/CLI for validating the Codefresh YAML
1,089 lines (990 loc) • 42.7 kB
JavaScript
/**
* The actual Validation module.
* Creates a Joi schema and tests the deserialized YAML descriptor
*/
;
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const Joi = require('joi');
const fs = require('fs');
const path = require('path');
const _ = require('lodash');
const colors = require('colors');
const Table = require('cli-table3');
const { SEMVER_REGEX } = require('./constants/semver-regex');
const ValidatorError = require('../../validator-error');
const BaseSchema = require('./base-schema');
const PendingApproval = require('./steps/pending-approval');
const { ErrorType, ErrorBuilder } = require('./error-builder');
const { docBaseUrl, DocumentationLinks, CustomDocumentationLinks } = require('./documentation-links');
const { StepValidator } = require('./constants/step-validator');
const SuggestArgumentValidation = require('./validations/suggest-argument');
const { JSONPathsGenerator } = require('./jsonpaths/jsonpaths-generator');
const { VARIABLE_REGEX, VARIABLE_EXACT_REGEX } = require('./constants/variable-regex');
const { RootSchema } = require('./root-schema');
/**
* ⬇️ Backward compatibility section
* This is a closed list of step types that are known to the Planner and processed in a special way,
* thus root-level properties controlled by us.
* Needed for backward compatibility while migrating to the new schema.
* Old schema:
* step:
* title: my-step
* type: freestyle
* image: alpine
* New schema:
* step:
* title: my-step
* type: freestyle
* arguments:
* image: alpine
* ⚠️ When adding a new property, common to all steps, ensure to add it to the `NEW_COMMON_PROPS` array
* in order to prevent conflicts with user-defined typed steps that already have this property in arguments.
*/
const { KNOWN_STEP_TYPES } = require('./constants/known-step-types');
const NEW_COMMON_PROPS = ['timeout'];
// ⬆️ The end.
let totalErrors;
let totalWarnings;
const MaxStepLength = 150;
function getAllStepNamesFromObjectModel(objectModelSteps, stepNameLst = []) {
_.flatMap(objectModelSteps, (step, key) => {
stepNameLst.push(key);
if (step.steps) {
getAllStepNamesFromObjectModel(step.steps, stepNameLst);
}
});
return stepNameLst;
}
class Validator {
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
static _throwValidationErrorAccordingToFormat(outputFormat) {
Validator._sortErrorAccordingLineNumber();
const err = new ValidatorError(totalErrors);
switch (outputFormat) {
case 'printify':
Validator._printify(err);
break;
case 'message':
Validator._message(err);
break;
case 'lint':
Validator._lint(err);
break;
default:
throw err;
}
}
static _throwValidationErrorAccordingToFormatWithWarnings(outputFormat) {
Validator._sortErrorAccordingLineNumber();
const err = new ValidatorError(totalErrors);
if (totalWarnings) {
Validator._sortWarningAccordingLineNumber();
err.warningDetails = totalWarnings.details;
}
switch (outputFormat) {
case 'printify':
Validator._printify(err);
break;
case 'message':
Validator._message(err);
break;
case 'lint':
Validator._lintWithWarnings(err);
break;
default:
throw err;
}
}
static _addError(error) {
totalErrors.details = _.concat(totalErrors.details, error.details);
}
static _addWarning(warning) {
totalWarnings.details = _.concat(totalWarnings.details, warning.details);
}
static _sortWarningAccordingLineNumber() {
totalWarnings.details = _.sortBy(totalWarnings.details, [error => error.lines]);
}
static _sortErrorAccordingLineNumber() {
totalErrors.details = _.sortBy(totalErrors.details, [error => error.lines]);
}
static _printify(err) {
_.forEach(totalErrors.details, (error) => {
const table = new Table({
style: { header: [] },
colWidths: [20, 100],
wordWrap: true,
});
if (error.message) {
table.push({ [colors.red('Message')]: colors.red(error.message) });
}
if (error.type) {
table.push({ [colors.green('Error Type')]: error.type });
}
if (error.level) {
table.push({ [colors.green('Error Level')]: error.level });
}
if (error.stepName) {
table.push({ [colors.green('Step Name')]: error.stepName });
}
if (error.docsLink) {
table.push({ [colors.green('Documentation Link')]: error.docsLink });
}
if (error.actionItems) {
table.push({ [colors.green('Action Items')]: error.actionItems });
}
if (!_.isUndefined(error.lines)) {
table.push({ [colors.green('Error Lines')]: error.lines });
}
err.message += `\n${table.toString()}`;
});
throw err;
}
static _message(err) {
_.forEach(totalErrors.details, (error) => {
err.message += `${error.message}\n`;
});
throw err;
}
static _createTable() {
return new Table({
chars: {
'top': '',
'top-mid': '',
'top-left': '',
'top-right': '',
'bottom': '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
'left': '',
'left-mid': '',
'mid': '',
'mid-mid': '',
'right': '',
'right-mid': '',
'middle': ''
},
style: {
'head': [], 'border': [], 'padding-left': 1, 'padding-right': 1
},
colWidths: [5, 10, 80, 80],
wordWrap: true,
});
}
static _getSummarizeMessage() {
const warningCount = _.get(totalWarnings, 'details.length', 0);
const errorCount = _.get(totalErrors, 'details.length', 0);
const problemsCount = errorCount + warningCount;
let summarize = `✖ ${problemsCount}`;
if (problemsCount === 1) {
summarize += ' problem ';
} else {
summarize += ' problems ';
}
if (errorCount === 1) {
summarize += `(${errorCount} error, `;
} else {
summarize += `(${errorCount} errors, `;
}
if (warningCount === 1) {
summarize += `${warningCount} warning)`;
} else {
summarize += `${warningCount} warnings)`;
}
return summarize;
}
static _lint(err) {
err.message = `${colors.red('\n')}`;
const table = Validator._createTable();
_.forEach(totalErrors.details, (error) => {
table.push([error.lines, colors.red('error'), error.message, error.docsLink]);
});
err.message += `\n${table.toString()}\n`;
throw err;
}
static _lintWithWarnings(err) {
const table = Validator._createTable();
const warningTable = Validator._createTable();
const documentationLinks = new Set();
if (totalWarnings && !_.isEmpty(totalWarnings.details)) {
_.forEach(totalWarnings.details, (warning) => {
warningTable.push([warning.lines, colors.yellow('warning'), warning.message]);
documentationLinks.add(`Visit ${warning.docsLink} for ${warning.path} documentation\n`);
});
err.warningMessage = `${colors.yellow('Yaml validation warnings:\n')}`;
err.warningMessage += `\n${warningTable.toString()}\n`;
err.summarize = colors.yellow(Validator._getSummarizeMessage());
}
if (!_.isEmpty(totalErrors.details)) {
_.forEach(totalErrors.details, (error) => {
table.push([error.lines, colors.red('error'), error.message]);
documentationLinks.add(`Visit ${error.docsLink} for ${error.path} documentation\n`);
});
err.message = `${colors.red('Yaml validation errors:\n')}`;
err.message += `\n${table.toString()}\n`;
err.summarize = colors.red(Validator._getSummarizeMessage());
}
err.documentationLinks = '';
documentationLinks.forEach((documentationLink) => { err.documentationLinks += documentationLink; });
throw err;
}
static _validateStepsLength(objectModel, yaml) {
// get all step names:
const stepNamesList = getAllStepNamesFromObjectModel(objectModel.steps);
const currentMaxStepLength = stepNamesList.reduce((acc, curr) => {
if (curr.length > acc.length) {
acc = {
length: curr.length,
name: curr
};
}
return acc;
}, {
length: 0
});
if (currentMaxStepLength.length > MaxStepLength) {
const message = `step name length is limited to ${MaxStepLength}`;
const stepName = currentMaxStepLength.name;
Validator._addError({
message,
name: 'ValidationError',
details: [
{
message,
type: 'Validation',
path: 'steps',
context: {
key: 'steps',
},
level: 'step',
stepName,
docsLink: 'https://codefresh.io/docs/docs/codefresh-yaml/advanced-workflows/#parallel-pipeline-mode',
actionItems: `Please shoten name for ${stepName} steps`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName }),
},
]
});
}
}
static _validateUniqueStepNames(objectModel, yaml) {
// get all step names:
const stepNamesList = getAllStepNamesFromObjectModel(objectModel.steps);
// get duplicate step names
const duplicateSteps = _.filter(stepNamesList, (stepName, index, iteratee) => _.includes(iteratee, stepName, index + 1));
if (duplicateSteps.length > 0) {
_.forEach(duplicateSteps, (stepName) => {
const message = `step name exist more than once`;
const error = new Error(message);
error.name = 'ValidationError';
error.isJoi = true;
error.details = [
{
message,
type: 'Validation',
path: 'steps',
context: {
key: 'steps',
},
level: 'step',
stepName,
docsLink: 'https://codefresh.io/docs/docs/codefresh-yaml/advanced-workflows/#parallel-pipeline-mode',
actionItems: `Please rename ${stepName} steps`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName }),
},
];
Validator._addError(error);
});
}
}
static _validateStepsNames(objectModel, yaml) {
// get all step names:
const stepNamesList = getAllStepNamesFromObjectModel(objectModel.steps);
// eslint-disable-next-line no-useless-escape
const stepNameRegex = /^[^.#$\[\]]*$/;
stepNamesList.forEach((stepName) => {
if (!stepNameRegex.test(stepName)) {
// eslint-disable-next-line no-useless-escape
const message = `step name cannot contain \".\", \"#\", \"$\", \"[\", or \"]\"`;
Validator._addError({
message,
name: 'ValidationError',
details: [
{
message,
type: 'Validation',
path: 'steps',
context: {
key: 'steps',
},
level: 'step',
stepName,
docsLink: 'https://codefresh.io/docs/docs/codefresh-yaml/advanced-workflows/#parallel-pipeline-mode',
actionItems: `Please change the name for '${stepName}' step`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName }),
},
]
});
}
});
}
static _validateRootSchema(objectModel, yaml) {
const validationResult = Joi.validate(objectModel, RootSchema.getSchema(), { abortEarly: false });
if (validationResult.error) {
_.forEach(validationResult.error.details, (err) => {
Validator._processRootSchemaError(err, validationResult, yaml);
});
}
}
static _processRootSchemaError(err, validationResult, yaml) {
// regex to split joi's error path so that we can use lodah's _.get
// we make sure split first ${{}} annotations before splitting by dots (.)
const joiPathSplitted = err.path
.split(/(\$\{\{[^}]*}})|([^.]+)/g);
// TODO: I (Itai) put this code because i could not find a good regex to do all the job
const originalPath = [];
_.forEach(joiPathSplitted, (keyPath) => {
if (keyPath && keyPath !== '.') {
originalPath.push(keyPath);
}
});
const originalFieldValue = _.get(validationResult, ['value', ...originalPath]);
const message = originalFieldValue ? `${err.message}. Current value: ${originalFieldValue} ` : err.message;
const error = new Error();
error.name = 'ValidationError';
error.isJoi = true;
error.details = [
{
message,
type: 'Validation',
path: 'workflow',
context: {
key: 'workflow',
},
level: 'workflow',
docsLink: 'https://codefresh.io/docs/docs/codefresh-yaml/what-is-the-codefresh-yaml/',
actionItems: `Please make sure you have all the required fields`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, key: err.path }),
},
];
Validator._addError(error);
}
static _resolveStepsModules() {
if (this.stepsModules) {
return this.stepsModules;
}
const stepsPath = path.join(__dirname, 'steps');
const allStepSchemaFiles = fs.readdirSync(stepsPath);
const stepsModules = {};
allStepSchemaFiles.forEach(((schemaFile) => {
const StepModule = require(path.join(stepsPath, schemaFile)); // eslint-disable-line
if (StepModule.getType()) {
stepsModules[StepModule.getType()] = StepModule;
}
}));
this.stepsModules = stepsModules;
return this.stepsModules;
}
static _resolveStepsJoiSchemas(objectModel = {}, opts = {}) {
const stepsModules = Validator._resolveStepsModules();
const joiSchemas = {};
_.forEach(stepsModules, (StepModule, stepType) => {
joiSchemas[stepType] = new StepModule(objectModel).getSchema(opts[stepType]);
});
return joiSchemas;
}
static _validateStepSchema(objectModel, yaml, opts = {}) {
const stepsSchemas = Validator._resolveStepsJoiSchemas(objectModel, opts);
const steps = Validator._getFormattedSteps(objectModel.steps, yaml);
for (const stepName in steps) { // eslint-disable-line
const step = steps[stepName];
Validator._validateSingleStepSchema(step, stepsSchemas, stepName, yaml, opts);
}
}
static _getFormattedSteps(stepsModel, yaml) {
const steps = {};
_.map(stepsModel, (s, name) => {
const step = _.cloneDeep(s);
if (step.arguments) {
Validator._assignArgumentsToStep(step);
}
if (step.type === 'parallel') {
Validator._validateParallelTypeInnerSteps(step, steps, yaml);
} else {
steps[name] = step;
}
});
return steps;
}
static _assignArgumentsToStep(step) {
const clonedArguments = _.cloneDeep(step.arguments);
if (KNOWN_STEP_TYPES.includes(step.type) || !step.type) {
_.assign(step, clonedArguments);
} else {
_.assign(step, _.omit(clonedArguments, NEW_COMMON_PROPS));
}
delete step.arguments;
}
static _validateParallelTypeInnerSteps(step, steps, yaml) {
if (_.size(step.steps) > 0) {
_.map(step.steps, (innerStep, innerName) => {
steps[innerName] = innerStep;
});
for (const stepName in step.steps) { // eslint-disable-line
const subStep = steps[stepName];
if (_.get(subStep, 'type', 'freestyle') === PendingApproval.getType()) {
const error = new Error(`"type" can't be ${PendingApproval.getType()}`);
error.name = 'ValidationError';
error.isJoi = true;
error.details = [
{
message: `"type" can't be ${PendingApproval.getType()}`,
type: 'Validation',
path: 'type',
context: {
key: 'type',
},
level: 'step',
stepName,
docsLink: 'https://codefresh.io/docs/docs/codefresh-yaml/advanced-workflows/',
actionItems: `Please change the type of the sub step`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName }),
},
];
Validator._addError(error);
}
}
} else {
const error = new Error('"steps" is required and must be an array steps');
error.name = 'ValidationError';
error.isJoi = true;
error.details = [
{
message: '"steps" is required and must be an array of type steps',
type: 'Validation',
path: 'steps',
context: {
key: 'steps',
},
level: 'workflow',
docsLink: 'https://codefresh.io/docs/docs/codefresh-yaml/what-is-the-codefresh-yaml/',
actionItems: `Please make sure you have all the required fields`,
lines: ErrorBuilder.getErrorLineNumber({ yaml }),
},
];
Validator._addError(error);
}
}
static _validateSingleStepSchema(step, stepsSchemas, stepName, yaml, opts) {
let { type } = step;
if (!type) {
type = 'freestyle';
}
Validator._validateStepType(step, stepsSchemas, stepName, yaml, opts);
let stepSchema = stepsSchemas[type];
if (!stepSchema) {
console.log(`Warning: no schema found for step type '${type}'. Skipping validation`);
return;
}
if (opts.isInHook) {
const hookStepSchema = stepSchema.keys({
debug: Joi.forbidden(),
on_start: Joi.forbidden(),
on_finish: Joi.forbidden(),
on_fail: Joi.forbidden(),
hooks: Joi.forbidden(),
stage: Joi.forbidden(),
});
stepSchema = hookStepSchema;
}
const validationResult = Joi.validate(step, stepSchema, { abortEarly: false });
if (validationResult.error) {
_.forEach(validationResult.error.details, (err) => {
Validator._processStepSchemaError(err, validationResult, stepName, type, yaml, stepSchema);
});
}
}
static _processStepSchemaError(err, validationResult, stepName, type, yaml, stepSchema) {
if (type === 'parallel') return; // TODO: add all errors from parallel step to warnings
const originalPath = Validator._getOriginalPath(err);
const originalFieldValue = Validator._getOriginalFieldValue(originalPath, validationResult);
const suggestion = Validator._getArgumentSuggestion(err, originalPath, stepSchema);
const message = Validator._getStepSchemaErrorMessage(err, originalFieldValue, suggestion);
// TODO: Delete once timeout is required. ⬇️
if (type !== 'pending-approval' && type !== 'deploy' && err.path === 'timeout') {
const warning = new Error(message);
warning.name = 'ValidationError';
warning.isJoi = true;
warning.details = [
{
message,
type: ErrorType.Warning,
path: 'steps',
context: {
key: 'steps',
},
level: 'step',
stepName,
docsLink: _.get(DocumentationLinks, `${type}`, docBaseUrl),
// eslint-disable-next-line max-len
actionItems: `Please adjust the timeout to match the format "<duration><units>" where duration is int|float and units are s|m|h (e.g. "1m", "30s", "1.5h"). It will be ignored otherwise.`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName, key: err.path }),
},
];
Validator._addWarning(warning);
return;
}
// END: Delete once timeout is required. ⬆️
const error = new Error();
error.name = 'ValidationError';
error.isJoi = true;
const errorDetails = {
message,
type: 'Validation',
path: 'steps',
context: {
key: 'steps',
},
level: 'step',
stepName,
docsLink: _.get(DocumentationLinks, `${type}`, docBaseUrl),
actionItems: `Please make sure you have all the required fields and valid values`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName, key: err.path }),
};
if (suggestion) {
errorDetails.suggestion = {
from: err.context.key,
to: suggestion,
};
}
error.details = [_.pickBy(errorDetails, _.identity)];
Validator._addError(error);
}
static _validateStepType(step, stepsSchemas, stepName, yaml) {
const rawType = step.type || 'freestyle';
const [type, ...rest] = rawType.split(':');
const stepVersion = rest.join(':');
if (
rawType.match(VARIABLE_REGEX)
|| KNOWN_STEP_TYPES.includes(rawType)
|| KNOWN_STEP_TYPES.includes(type)
) {
return;
}
const versionSchema = Joi.string().regex(SEMVER_REGEX).required();
const validationResult = Joi.validate(stepVersion, versionSchema);
if (validationResult.error) {
validationResult.error.details.forEach((errDetails) => {
Validator._processStepVersionError(
errDetails,
validationResult,
stepName,
rawType,
yaml,
stepsSchemas[type],
type,
stepVersion
);
});
}
}
static _processStepVersionError(errDetails, _validationResult, stepName, _type, yaml, _stepSchema, parsedType, parsedVersion) {
if (errDetails.message === `"value" is required` || errDetails.message === `"value" is not allowed to be empty`) {
const message = `Step version not specified. The latest version will be used, which may result in breaking changes.`;
const warning = new Error(message);
warning.name = 'ValidationError';
warning.isJoi = true;
warning.details = [
{
message,
type: ErrorType.Warning,
path: 'steps',
context: {
key: 'steps',
},
level: 'step',
stepName,
docsLink: CustomDocumentationLinks['steps-versioning'],
// eslint-disable-next-line max-len
actionItems: `To specify a version for the step, add the version number to the "type" flag. For example, "type: ${parsedType}:1.2.3"`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName, key: 'type' }),
},
];
Validator._addWarning(warning);
return;
}
if (errDetails.message.includes('fails to match the required pattern')) {
const message = `Invalid semantic version "${parsedVersion}" for step.`;
const warning = new Error(message);
warning.name = 'ValidationError';
warning.isJoi = true;
warning.details = [
{
message,
type: ErrorType.Warning,
path: 'steps',
context: {
key: 'steps',
},
level: 'step',
stepName,
docsLink: CustomDocumentationLinks['steps-versioning'],
actionItems: `Use "type: <type_name>:<version-number>". For example, "type: ${parsedType}:1.2.3"`,
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName, key: 'type' }),
},
];
Validator._addWarning(warning);
return;
}
const { message } = errDetails;
const error = new Error(message);
error.name = 'ValidationError';
error.isJoi = true;
error.details = [
{
message,
type: ErrorType.Error,
path: 'steps',
context: {
key: 'steps',
},
level: 'step',
stepName,
docsLink: CustomDocumentationLinks['steps-versioning'],
lines: ErrorBuilder.getErrorLineNumber({ yaml, stepName, key: 'type' }),
},
];
Validator._addError(error);
}
static _getOriginalPath(err) {
// regex to split joi's error path so that we can use lodah's _.get
// we make sure split first ${{}} annotations before splitting by dots (.)
const joiPathSplitted = err.path
.split(/(\$\{\{[^}]*}})|([^.]+)/g);
// TODO: I (Itai) put this code because i could not find a good regex to do all the job
const originalPath = [];
_.forEach(joiPathSplitted, (keyPath) => {
if (keyPath && keyPath !== '.') {
originalPath.push(keyPath);
}
});
return originalPath;
}
static _getOriginalFieldValue(originalPath, validationResult) {
return _.get(validationResult, ['value', ...originalPath]);
}
static _getArgumentSuggestion(err, originalPath, stepSchema) {
const isNotAllowedArgumentError = _.includes(_.get(err, 'message'), 'is not allowed');
const misspelledArgument = _.get(err, 'context.key', '');
const suggestion = SuggestArgumentValidation.suggest(stepSchema, misspelledArgument, originalPath.slice(0, originalPath.length - 1));
const canSuggest = !!(isNotAllowedArgumentError && misspelledArgument && stepSchema && suggestion);
return canSuggest ? suggestion : null;
}
static _getStepSchemaErrorMessage(err, originalFieldValue, suggestion) {
const isNotAllowedArgumentError = _.includes(_.get(err, 'message'), 'is not allowed');
if (suggestion) {
return `${err.message}. Did you mean "${suggestion}"?`;
}
if (originalFieldValue && !isNotAllowedArgumentError) {
return `${err.message}. Current value: ${originalFieldValue} `;
}
return err.message;
}
static _validateContextStep(objectModel, yaml, context, opts) {
const ignoreValidation = _.get(opts, 'ignoreValidation', false);
_.forEach(objectModel.steps, (s, name) => {
const step = _.cloneDeep(s);
const stepType = _.get(step, 'type', 'freestyle');
const validation = _.get(StepValidator, stepType);
if (validation) {
const { errors, warnings } = validation.validateStep(step, yaml, name, context, { ignoreValidation });
errors.forEach(error => Validator._addError(error));
warnings.forEach(warning => Validator._addWarning(warning));
}
if (step.type === 'parallel' || step.steps) {
this._validateContextStep(step, yaml, context, opts);
}
});
}
static _validateIndention(yaml, outputFormat) {
const yamlArray = yaml.split('\n');
_.forEach(yamlArray, (line, number) => {
if (line.match('(\\t+\\s+|\\s+\\t+)')) {
const error = new Error('Mix of tabs and spaces');
error.name = 'ValidationError';
error.isJoi = true;
error.details = [
{
message: 'Your YAML contains both spaces and tabs.',
type: ErrorType.Error,
path: 'indention',
code: 400,
context: {
key: 'indention',
},
level: 'workflow',
docsLink: 'https://codefresh.io/docs/docs/codefresh-yaml/what-is-the-codefresh-yaml/',
lines: number,
actionItems: 'Please replace all tabs with spaces.'
},
];
Validator._addError(error);
}
});
if (_.size(totalErrors.details) > 0) {
// throw error because when pipeline have a mix of tabs and spaces it not pass other validation
Validator._throwValidationErrorAccordingToFormatWithWarnings(outputFormat);
}
}
static _validateNewLineToSpaceConverter(yaml) {
const yamlArray = yaml.split('\n');
let validation = false;
let prevSpaceCount = 0;
for (const number in yamlArray) { // eslint-disable-line
const line = yamlArray[number];
if (line.includes('- >-')) {
validation = true;
const nextNumber = Number(number) + 1;
if ((nextNumber) < (yamlArray.length - 1)) {
prevSpaceCount = yamlArray[nextNumber].search(/\S/);
}
} else if (validation) {
const spaceCount = line.search(/\S/);
if (line.includes('- ')
|| (spaceCount < prevSpaceCount && line.match('^((?!-|\'|"|`).)*(([a-zA-Z/-\\\\]:\\s*[a-zA-Z-/-\\\\\'".]*))$'))) {
validation = false;
} else if (spaceCount > prevSpaceCount) {
const error = new Error('Bad indention in commands step');
error.name = 'ValidationError';
error.isJoi = true;
error.details = [
{
message: `Your YAML contains invalid indentation after characters '>-'.`,
type: ErrorType.Warning,
path: 'indention',
code: 500,
context: {
key: 'indention',
},
level: 'workflow',
docsLink: 'https://codefresh.io/docs/docs/codefresh-yaml/what-is-the-codefresh-yaml/',
lines: Number(number) + 1,
actionItems: `Align the indent to the first line after characters '>-'.`
},
];
Validator._addWarning(error);
}
}
}
}
static _validateHooksSchema(objectModel, yaml, opts = {}) {
// if there are any pipeline hooks, validate them one by one
if (objectModel.hooks) {
_.forEach(objectModel.hooks, (hook) => {
Validator._validateSingleHookSchema(objectModel, hook, yaml, opts);
});
}
// check for each step if it has hooks and validate them
_.forEach(objectModel.steps, (step, stepName) => {
if (step.hooks) {
_.forEach(step.hooks, (hook) => {
Validator._validateSingleHookSchema(objectModel, hook, yaml, opts);
});
const validationResult = Validator._validateDisallowOldHooks(step);
delete validationResult.value;
if (validationResult.error) {
_.forEach(validationResult.error.details, (err) => {
Validator._processStepSchemaError(err, validationResult, stepName, 'freestyle', yaml);
});
}
}
});
}
static _validateSingleHookSchema(objectModel, hook, yaml, opts = {}) {
if (_.isArray(hook.exec) && !hook.metadata && !hook.annotations) {
return {};
}
if (_.isArray(hook)) {
return {};
}
// in case a hook contains only metadata or annotations
if (!hook.exec && !hook.steps && (hook.metadata || hook.annotations)) {
const hookSchema = Joi.object({
metadata: BaseSchema._getMetadataSchema(),
annotations: BaseSchema._getAnnotationsSchema(),
});
const validationResult = Joi.validate(hook, hookSchema, { abortEarly: false });
if (validationResult.error) {
_.forEach(validationResult.error.details, (err) => {
return Validator._processStepSchemaError(err, validationResult, hook.name, 'freestyle', yaml);
});
}
return {};
}
const stepsSchemas = Validator._resolveStepsJoiSchemas(objectModel, opts);
let steps = {};
// gathering the steps from relevant position
if (hook.exec && hook.exec.steps) {
steps = Validator._getFormattedSteps(hook.exec.steps, yaml);
} else if (!hook.exec && hook.steps) {
steps = Validator._getFormattedSteps(hook.steps, yaml);
} else if (hook.exec && !hook.exec.steps) {
const step = _.cloneDeep(hook.exec);
if (step.arguments) {
Validator._assignArgumentsToStep(step);
}
steps[step.name] = step;
} else if (!hook.exec && !hook.steps) {
const step = _.cloneDeep(hook);
if (step.arguments) {
Validator._assignArgumentsToStep(step);
}
steps[step.name] = step;
}
// validating the steps seperately
for (const stepName in steps) { // eslint-disable-line
opts.isInHook = true;
const step = steps[stepName];
Validator._validateSingleStepSchema(step, stepsSchemas, stepName, yaml, opts);
}
let hookSchema = {};
const multipleStepsSchema = Joi.object({
mode: Joi.string().valid('sequential', 'parallel'),
fail_fast: BaseSchema.getBooleanSchema(),
strict_fail_fast: BaseSchema.getBooleanSchema({ strictBoolean: true }).optional(),
steps: Joi.object().pattern(/^.+$/, Joi.object()),
});
let execSchema = Joi.alternatives([
Joi.array().items(Joi.string()),
Joi.object()
]);
if (hook.exec) {
if (hook.exec.steps) {
execSchema = multipleStepsSchema;
}
hookSchema = Joi.object({
exec: execSchema,
metadata: BaseSchema._getMetadataSchema(),
annotations: BaseSchema._getAnnotationsSchema(),
});
} else if (hook.steps) {
hookSchema = multipleStepsSchema;
} else {
hookSchema = Joi.object();
}
// Validating the hook's structure schema
const validationResult = Joi.validate(hook, hookSchema, { abortEarly: false });
if (validationResult.error) {
_.forEach(validationResult.error.details, (err) => {
return Validator._processStepSchemaError(err, validationResult, hook.name, 'freestyle', yaml);
});
}
return {};
}
static _validateDisallowOldHooks(step) {
const { hooks } = step;
if (hooks && (hooks.on_success || hooks.on_finish || hooks.on_fail)) {
const message = 'Either old "on_success/on_fail/on_finish" or new "hooks" should be used';
const schema = Joi.object({
on_success: Joi.forbidden().error(ErrorBuilder.buildJoiError({ message, path: 'on_success' })),
on_finish: Joi.forbidden().error(ErrorBuilder.buildJoiError({ message, path: 'on_finish' })),
on_fail: Joi.forbidden().error(ErrorBuilder.buildJoiError({ message, path: 'on_fail' })),
}).unknown(true);
return Joi.validate(step, schema, { abortEarly: false });
}
return {};
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Validates a model of the deserialized YAML
*
* @param objectModel Deserialized YAML
* @param outputFormat desire output format YAML
* @throws An error containing the details of the validation failure
*/
static validate(objectModel, outputFormat = 'message', yaml, opts) {
totalErrors = {
details: [],
};
totalWarnings = {
details: [],
};
Validator._validateStepsNames(objectModel, yaml);
Validator._validateUniqueStepNames(objectModel, yaml);
Validator._validateStepsLength(objectModel, yaml);
Validator._validateRootSchema(objectModel, yaml);
Validator._validateStepSchema(objectModel, yaml, opts);
Validator._validateHooksSchema(objectModel, yaml, opts);
if (_.size(totalErrors.details) > 0 || _.size(totalWarnings.details) > 0) {
Validator._throwValidationErrorAccordingToFormatWithWarnings(outputFormat);
}
}
/**
* Validates a model of the deserialized YAML
*
* @param objectModel Deserialized YAML
* @param outputFormat desire output format YAML
* @param yaml as string
* @param context by account with git, clusters and registries
* @throws An error containing the details of the validation failure
*/
static validateWithContext(objectModel, outputFormat = 'message', yaml, context, opts) {
totalErrors = {
details: [],
};
totalWarnings = {
details: [],
};
Validator._validateStepsNames(objectModel, yaml);
Validator._validateIndention(yaml, outputFormat);
Validator._validateNewLineToSpaceConverter(yaml);
Validator._validateUniqueStepNames(objectModel, yaml);
Validator._validateStepsLength(objectModel, yaml);
Validator._validateRootSchema(objectModel, yaml);
Validator._validateStepSchema(objectModel, yaml, opts);
Validator._validateHooksSchema(objectModel, yaml);
Validator._validateContextStep(objectModel, yaml, context, opts);
if (_.size(totalErrors.details) > 0 || _.size(totalWarnings.details) > 0) {
Validator._throwValidationErrorAccordingToFormatWithWarnings(outputFormat);
}
}
static getJsonSchemas() {
if (this.jsonSchemas) {
return this.jsonSchemas;
}
const stepsModules = Validator._resolveStepsModules();
const jsonSchemas = {};
_.forEach(stepsModules, (StepModule, stepType) => {
jsonSchemas[stepType] = new StepModule({}).getJsonSchema();
});
this.jsonSchemas = jsonSchemas;
return this.jsonSchemas;
}
static getRootJoiSchema() {
return RootSchema.getSchema();
}
static getStepsJoiSchemas() {
if (this._stepsJoiSchemas) {
return this._stepsJoiSchemas;
}
this._stepsJoiSchemas = this._resolveStepsJoiSchemas({}, {
build: {
buildVersion: 'V2', // use the fullest schema
}
});
return this._stepsJoiSchemas;
}
static generateJSONPaths({ fieldType, joiSchema, convertToCamelCase }) {
return new JSONPathsGenerator({ fieldType, joiSchema, convertToCamelCase }).getJSONPaths();
}
static getVariableRegex({ isExact } = {}) {
return isExact ? VARIABLE_EXACT_REGEX : VARIABLE_REGEX;
}
}
// Exported objects/methods
module.exports = Validator;