diff --git a/lib/interface/cli/commands/root/create.cmd.js b/lib/interface/cli/commands/root/create.cmd.js index 7093b703a..53a4be07e 100644 --- a/lib/interface/cli/commands/root/create.cmd.js +++ b/lib/interface/cli/commands/root/create.cmd.js @@ -3,7 +3,7 @@ const Command = require('../../Command'); const { crudFilenameOption } = require('../../helpers/general'); const { context, pipeline } = require('../../../../logic').api; const yargs = require('yargs'); -const { validatePipelineFile } = require('../../helpers/validation'); +const { validatePipelineSpec } = require('../../helpers/validation'); const get = new Command({ root: true, @@ -40,10 +40,9 @@ const get = new Command({ console.log(`Context: ${name} created`); break; case 'pipeline': - try { - await validatePipelineFile(data); - } catch (e) { - console.warn(e.message); + const result = await validatePipelineSpec(data); + if (!result.valid) { + console.warn(result.message); return; } await pipeline.createPipeline(data); diff --git a/lib/interface/cli/commands/root/replace.cmd.js b/lib/interface/cli/commands/root/replace.cmd.js index 05fed7de1..8c82c90bc 100644 --- a/lib/interface/cli/commands/root/replace.cmd.js +++ b/lib/interface/cli/commands/root/replace.cmd.js @@ -3,7 +3,7 @@ const Command = require('../../Command'); const { crudFilenameOption } = require('../../helpers/general'); const { context, pipeline } = require('../../../../logic').api; const yargs = require('yargs'); -const { validatePipelineFile } = require('../../helpers/validation'); +const { validatePipelineSpec } = require('../../helpers/validation'); const annotate = new Command({ root: true, @@ -39,10 +39,9 @@ const annotate = new Command({ console.log(`Context: ${name} created`); break; case 'pipeline': - try { - await validatePipelineFile(data); - } catch (e) { - console.warn(e.message); + const result = await validatePipelineSpec(data); + if (!result.valid) { + console.warn(result.message); return; } await pipeline.replaceByName(name, data); diff --git a/lib/interface/cli/commands/root/validate.cmd.js b/lib/interface/cli/commands/root/validate.cmd.js new file mode 100644 index 000000000..59bf98ec1 --- /dev/null +++ b/lib/interface/cli/commands/root/validate.cmd.js @@ -0,0 +1,92 @@ +const Command = require('../../Command'); +const _ = require('lodash'); +const Style = require('../../../../output/style'); +const path = require('path'); +const { validatePipelineYaml } = require('../../helpers/validation'); +const { pathExists, watchFile } = require('../../helpers/general'); + +const VALID_MESSAGE = Style.green('Yaml is valid!'); + +function _printResult(result) { + console.log(result.valid ? VALID_MESSAGE : Style.red(result.message), '\n'); +} + +const validateCmd = new Command({ + root: true, + command: 'validate [filenames..]', + description: 'Validate codefresh pipeline yaml config files.', + usage: 'Validate one or many pipeline yaml files or attach validator to one and validate on changes', + webDocs: { + description: 'Validate codefresh pipeline yaml config files', + category: 'Validation', + title: 'Validate pipeline yaml', + weight: 100, + }, + builder: (yargs) => { + yargs + .positional('filenames', { + describe: 'Paths to yaml files', + required: true, + }) + .option('attach', { + alias: 'a', + describe: 'Attach validator to the file and validate on change', + }); + + + return yargs; + }, + handler: async (argv) => { + let { filenames, attach } = argv; + filenames = filenames.map(f => path.resolve(process.cwd(), f)); + + if (_.isEmpty(filenames)) { + console.log('No filename provided!'); + return; + } + + const checkPromises = filenames.map((filename) => { + return pathExists(filename) + .then((exists) => { + if (!exists) { + console.log(`File does not exist: ${filename}`); + } + return exists; + }); + }); + const allExist = (await Promise.all(checkPromises)).reduce((a, b) => a && b); + + if (!allExist) { + return; + } + + if (filenames.length > 1) { + if (attach) { + console.log('Cannot watch many files!'); + return; + } + + filenames.forEach(f => validatePipelineYaml(f).then((result) => { + console.log(`Validation result for ${f}:`); + _printResult(result); + })); + return; + } + + const filename = filenames[0]; + if (attach) { + console.log(`Validator attached to file: ${filename}`); + watchFile(filename, async () => { + console.log('File changed'); + const result = await validatePipelineYaml(filename); + _printResult(result); + }); + } + + // even with --attach option validates file for first time + const result = await validatePipelineYaml(filename); + _printResult(result); + }, +}); + +module.exports = validateCmd; diff --git a/lib/interface/cli/helpers/general.js b/lib/interface/cli/helpers/general.js index 17e8c8107..602949f5b 100644 --- a/lib/interface/cli/helpers/general.js +++ b/lib/interface/cli/helpers/general.js @@ -126,10 +126,29 @@ const crudFilenameOption = (yargs, options = {}) => { }); }; +function pathExists(p) { + return new Promise(resolve => fs.access(p, resolve)) + .then(err => !err); +} + +const readFile = Promise.promisify(fs.readFile); + +function watchFile(filename, listener) { + fs.watchFile(filename, { interval: 500 }, listener); + const unwatcher = f => () => fs.unwatchFile(f); + ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'SIGTERM'].forEach((eventType) => { + process.on(eventType, unwatcher(filename)); + }); +} + + module.exports = { printError, wrapHandler, prepareKeyValueFromCLIEnvOption, crudFilenameOption, prepareKeyValueCompostionFromCLIEnvOption, + pathExists, + readFile, + watchFile, }; diff --git a/lib/interface/cli/helpers/validation.js b/lib/interface/cli/helpers/validation.js index 7a236efcb..ee9058873 100644 --- a/lib/interface/cli/helpers/validation.js +++ b/lib/interface/cli/helpers/validation.js @@ -1,23 +1,40 @@ const _ = require('lodash'); const yaml = require('js-yaml'); const { pipeline } = require('../../../logic').api; +const { readFile } = require('./general'); -async function validatePipelineFile(data) { +function _buildFinalMessage(baseMessage, validationResult) { + if (_.isArray(validationResult.details)) { + const errors = validationResult.details + .map(({ message }) => ` - ${message}`) + .join('\n'); + return `${baseMessage}:\n${errors}`; + } + return `${baseMessage}!`; +} + +async function validatePipelineSpec(data) { const validatedYaml = yaml.safeDump(Object.assign({ version: data.version }, data.spec)); - const validationResult = await pipeline.validateYaml(validatedYaml); - if (!validationResult.valid) { - let finalMessage; - if (_.isArray(validationResult.details)) { - const errors = validationResult.details.map(({ message }) => ` - ${message}`).join('\n'); - finalMessage = `Provided spec is not valid:\n${errors}`; - } else { - finalMessage = 'Provided spec is not valid!'; - } - throw new Error(finalMessage); + const result = await pipeline.validateYaml(validatedYaml); + let message; + if (!result.valid) { + message = _buildFinalMessage('Provided spec is not valid', result); + } + return { valid: !!result.valid, message }; +} + +async function validatePipelineYaml(filename) { + const yamlContent = await readFile(filename, 'UTF-8'); + const result = await pipeline.validateYaml(yamlContent); + let message; + if (!result.valid) { + message = _buildFinalMessage('Yaml not valid', result); } + return { valid: !!result.valid, message }; } module.exports = { - validatePipelineFile, + validatePipelineSpec, + validatePipelineYaml, }; diff --git a/package.json b/package.json index b4f1aebad..3f725334a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codefresh", - "version": "0.9.1", + "version": "0.9.2", "description": "Codefresh command line utility", "main": "index.js", "preferGlobal": true,