Skip to content

Commit

Permalink
feat(3124): Add validatePipelineTemplate function (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkyi authored Jun 13, 2024
1 parent cf1a7cc commit 60634ca
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 9 deletions.
46 changes: 42 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: [],
Expand All @@ -155,5 +192,6 @@ async function parsePipelineTemplate(yamlString) {

module.exports = {
parseJobTemplate,
parsePipelineTemplate
parsePipelineTemplate,
validatePipelineTemplate
};
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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
Expand Down
File renamed without changes.
178 changes: 178 additions & 0 deletions test/data/valid_full_pipeline_template_validated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
{
"errors": [],
"template": {
"namespace": "template_namespace",
"name": "template_name",
"version": "1.2.3",
"description": "template description",
"maintainer": "[email protected]",
"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": "[email protected]"
},
"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": "[email protected]"
},
"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"
}
}
}
}
}
50 changes: 48 additions & 2 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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/);
}));
});
});

0 comments on commit 60634ca

Please sign in to comment.