diff --git a/index.js b/index.js index 0a34950..0d6deee 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,10 @@ 'use strict'; const SCHEMA_CONFIG = require('screwdriver-data-schema').config.template.template; -const parser = require('screwdriver-config-parser').parsePipelineTemplate; +const { + parsePipelineTemplate: parseTemplate, + validatePipelineTemplate: validateTemplate +} = require('screwdriver-config-parser'); const Yaml = require('js-yaml'); const helper = require('./lib/helper'); @@ -80,7 +83,7 @@ async function flattenTemplate(templateObj, templateFactory) { /** * Parses the job configuration from a screwdriver-template.yaml - * @method parseTemplate + * @method parseJobTemplate * @param {String} yamlString Contents of screwdriver-template.yaml * @param {TemplateFactory} templateFactory Template Factory to get template from * @return {Promise} Promise that rejects if the configuration cannot be parsed @@ -135,7 +138,41 @@ async function parsePipelineTemplate(yamlString) { const configToValidate = await loadTemplate(yamlString); try { - const config = await parser({ yaml: yamlString }); + const config = await parseTemplate({ yaml: yamlString }); + + return { + errors: [], + template: config + }; + } catch (err) { + if (!err.details) { + throw err; + } + + return { + errors: err.details, + template: configToValidate + }; + } +} + +/** + * Validates the pipeline configuration from a screwdriver-template.yaml + * @method validatePipelineTemplate + * @param {String} yamlString Contents of screwdriver-template.yaml + * @param {TemplateFactory} templateFactory Template Factory to get template from + * @return {Promise} Promise that rejects if the configuration cannot be validated + * The promise will eventually resolve into: + * {Object} result + * {Object} result.template The validated template that was validated + * {Object[]} result.errors An array of objects related to validating + * the given template + */ +async function validatePipelineTemplate(yamlString, templateFactory) { + const configToValidate = await loadTemplate(yamlString); + + try { + const config = await validateTemplate({ yaml: yamlString, templateFactory }); return { errors: [], @@ -155,5 +192,6 @@ async function parsePipelineTemplate(yamlString) { module.exports = { parseJobTemplate, - parsePipelineTemplate + parsePipelineTemplate, + validatePipelineTemplate }; diff --git a/package.json b/package.json index 27197f4..c994189 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "screwdriver-template-validator", - "version": "7.0.0", + "version": "8.0.0", "description": "A module for validating a Screwdriver Template file", "main": "index.js", "scripts": { @@ -43,8 +43,8 @@ "@hapi/hoek": "^10.0.1", "joi": "^17.7.0", "js-yaml": "^4.1.0", - "screwdriver-data-schema": "^23.0.4", - "screwdriver-config-parser": "^10.0.0" + "screwdriver-data-schema": "^23.3.2", + "screwdriver-config-parser": "^10.2.0" }, "release": { "debug": false diff --git a/test/data/valid_full_pipeline_template.json b/test/data/valid_full_pipeline_template_parsed.json similarity index 100% rename from test/data/valid_full_pipeline_template.json rename to test/data/valid_full_pipeline_template_parsed.json diff --git a/test/data/valid_full_pipeline_template_validated.json b/test/data/valid_full_pipeline_template_validated.json new file mode 100644 index 0000000..6bd7cd9 --- /dev/null +++ b/test/data/valid_full_pipeline_template_validated.json @@ -0,0 +1,178 @@ +{ + "errors": [], + "template": { + "namespace": "template_namespace", + "name": "template_name", + "version": "1.2.3", + "description": "template description", + "maintainer": "name@domain.org", + "config": { + "jobs": { + "main": { + "image": "node:18", + "environment": { + "BAR": "foo", + "FOO": "foo", + "SD_TEMPLATE_FULLNAME": "template_namespace/parent", + "SD_TEMPLATE_NAME": "parent", + "SD_TEMPLATE_NAMESPACE": "template_namespace", + "SD_TEMPLATE_VERSION": "1.2.3" + }, + "settings": { + "email": "foo@example.com" + }, + "blockedBy": [ + "~main" + ], + "cache": true, + "description": "This is a description!", + "annotations": { + "foo": "a", + "bar": "b" + }, + "freezeWindows": [ + "* * ? * 1", + "0-59 0-23 * 1 ?" + ], + "parameters": { + "color": [ + "red", + "blue" + ], + "node-version": { + "value": "18" + } + }, + "provider": { + "accountId": 111111111111, + "buildRegion": "", + "clusterName": "sd-build-eks", + "computeType": "BUILD_GENERAL1_SMALL", + "debugSession": false, + "environmentType": "LINUX_CONTAINER", + "executor": "eks", + "executorLogs": false, + "name": "aws", + "privilegedMode": false, + "region": "us-west-2", + "role": "arn:aws:iam::111111111111:role/role" + }, + "requires": [ + "~commit" + ], + "secrets": [ + "GIT_KEY", + "NPM_TOKEN" + ], + "sourcePaths": [ + "src/A", + "src/AConfig" + ], + "templateId": 7754, + "order": [ + "install", + "test", + "other", + "echo" + ], + "steps": [ + { + "install": "npm install" + }, + { + "test": "npm test" + }, + { + "echo": "echo $FOO" + } + ] + }, + "test": { + "image": "node:18", + "environment": { + "BAR": "foo", + "FOO": "foo", + "SD_TEMPLATE_FULLNAME": "template_namespace/parent", + "SD_TEMPLATE_NAME": "parent", + "SD_TEMPLATE_NAMESPACE": "template_namespace", + "SD_TEMPLATE_VERSION": "1.2.3" + }, + "settings": { + "email": "foo@example.com" + }, + "blockedBy": [ + "~main" + ], + "cache": true, + "description": "This is a description!", + "annotations": { + "foo": "a", + "bar": "b" + }, + "freezeWindows": [ + "* * ? * 1", + "0-59 0-23 * 1 ?" + ], + "parameters": { + "color": [ + "red", + "blue" + ], + "node-version": { + "value": "18" + } + }, + "provider": { + "accountId": 111111111111, + "buildRegion": "", + "clusterName": "sd-build-eks", + "computeType": "BUILD_GENERAL1_SMALL", + "debugSession": false, + "environmentType": "LINUX_CONTAINER", + "executor": "eks", + "executorLogs": false, + "name": "aws", + "privilegedMode": false, + "region": "us-west-2", + "role": "arn:aws:iam::111111111111:role/role" + }, + "requires": [ + "~commit" + ], + "secrets": [ + "GIT_KEY", + "NPM_TOKEN" + ], + "sourcePaths": [ + "src/A", + "src/AConfig" + ], + "templateId": 7754, + "order": [ + "install", + "test", + "other", + "echo" + ], + "steps": [ + { + "install": "npm install" + }, + { + "test": "npm test" + }, + { + "echo": "echo $FOO" + } + ] + } + }, + "parameters": { + "user": { + "value": "sd-bot", + "description": "User running build" + } + } + } + } +} diff --git a/test/index.test.js b/test/index.test.js index 0b68334..c802171 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -176,10 +176,10 @@ describe('index test', () => { it('parses a valid yaml', () => validator(loadData(VALID_FULL_PIPELINE_TEMPLATE_PATH)).then(config => { assert.isObject(config); - assert.deepEqual(config, JSON.parse(loadData('valid_full_pipeline_template.json'))); + assert.deepEqual(config, JSON.parse(loadData('valid_full_pipeline_template_parsed.json'))); })); - it('validates a poorly structured template', () => + it('parses a poorly structured template', () => validator(loadData(BAD_STRUCTURE_PIPELINE_TEMPLATE_PATH)).then(result => { assert.deepEqual(result.template, JSON.parse(loadData('bad_structure_pipeline_template.json'))); assert.strictEqual(result.errors.length, 2); @@ -203,4 +203,50 @@ describe('index test', () => { assert.match(err, /YAMLException/); })); }); + + describe('validate pipeline template', () => { + const templateFactoryMock = { + getTemplate: sinon.stub(), + getFullNameAndVersion: sinon.stub() + }; + + beforeEach(() => { + template = JSON.parse(loadData('template.json')); + templateLockedStep = JSON.parse(loadData('template_locked_step.json'), templateFactoryMock); + + templateFactoryMock.getTemplate.resolves(template); + // eslint-disable-next-line global-require + validator = require('../index').validatePipelineTemplate; + }); + + it('validates a valid yaml', () => + validator(loadData(VALID_FULL_PIPELINE_TEMPLATE_PATH), templateFactoryMock).then(config => { + assert.isObject(config); + assert.deepEqual(config, JSON.parse(loadData('valid_full_pipeline_template_validated.json'))); + })); + + it('validates a poorly structured template', () => + validator(loadData(BAD_STRUCTURE_PIPELINE_TEMPLATE_PATH), templateFactoryMock).then(result => { + assert.deepEqual(result.template, JSON.parse(loadData('bad_structure_pipeline_template.json'))); + assert.strictEqual(result.errors.length, 2); + + // check required description + const missingField = hoek.reach(result.template, result.errors[0].path[0]); + + assert.strictEqual(result.errors[0].message, '"description" is required'); + assert.isUndefined(missingField); + + // check incorrect type + const chain = `${result.errors[1].path[0]}.${result.errors[1].path[1]}`; + const incorrectType = hoek.reach(result.template, chain).image; + + assert.strictEqual(result.errors[1].message, '"config.shared.image" must be a string'); + assert.isNumber(incorrectType); + }, assert.fail)); + + it('throws when validating incorrectly formatted yaml', () => + validator('main: :').then(assert.fail, err => { + assert.match(err, /YAMLException/); + })); + }); });